This is an automated email from the ASF dual-hosted git repository.

dashorst pushed a commit to branch wicket-8.x
in repository https://gitbox.apache.org/repos/asf/wicket.git

commit 11b9bc51f6aab2717dc5044e0fcf751d330c4dc8
Author: Emond Papegaaij <[email protected]>
AuthorDate: Thu Sep 10 13:21:13 2026 +0200

    Ask IAuthorizationStrategy about a resource served by a component
    
    isResourceAuthorized() is consulted in one place, the constructor of
    ResourceRequestHandler, which ResourceReferenceRequestHandler builds for
    every resource requested through a mounted or shared ResourceReference. A
    component handed an IResource instance instead built IResource.Attributes
    itself and called IResource#respond() from its own request listener, so it
    never reached that constructor and the strategy was never asked: 
ResourceLink,
    Image - through LocalizedImageResource, for its src and for every srcset 
entry
    - ImageButton, and AjaxDownloadBehavior in wicket-extensions. Which of the 
two
    happened was decided by the constructor the application used and by nothing 
in
    the rendered page: ResourceLink(String, ResourceReference) renders a 
resource
    URL and was authorized, ResourceLink(String, IResource) renders a listener 
URL
    and was not.
    
    Serve those resources through a ResourceRequestHandler as well, which is 
what
    ResourceReferenceRequestHandler#respond() already does for the other route.
    That keeps one enforcement point rather than adding a second one that can
    drift, and needs no new API. IResource#respond() states that rule in its
    javadoc, at the method a component would otherwise call directly. 
LocalizedImageResource#onResourceRequested() is
    final on a final class, so the single edit there covers Image and 
ImageButton
    both.
    
    In AjaxDownloadBehavior the handler is constructed before the completion
    cookie is added, because the constructor is what asks the strategy:
    wicket-ajaxdownload.js reads that cookie before it looks at the response 
body,
    and a buffered response replays a cookie ahead of the error, so a refused
    download that had already set it would be reported as a success.
    
    ExportToolbar builds its export links from an IResource, so a data table
    export is authorized now as well. ResourceStreamRequestHandler, which
    DownloadLink schedules, still streams without asking; the resource it serves
    is one it constructs itself around an IResourceStream, so no application
    restriction was ever attached to it.
    
    Attributes#getParameters() on these routes is now the empty PageParameters
    that ResourceRequestHandler substitutes rather than null, which is what the
    mounted route has always passed, and the strategy is never asked with null.
    The parameters a component holds are not forwarded: ResourceLink leaves them
    null on this route by construction, and the ones in LocalizedImageResource
    belong to the reference route.
    
    An application that restricts a resource and hands it to one of those
    components now gets the refusal it configured where the resource used to be
    served. ResourceLink#onClick() no longer runs for a refused resource; it 
used
    to run after the bytes had been written.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 .../role/annotations/AuthorizeResource.java        |   4 +
 .../annotations/AnnotationsRoleResourceTest.java   | 222 ++++++++++
 .../authorization/IAuthorizationStrategy.java      |  16 +-
 .../image/resource/LocalizedImageResource.java     |  28 +-
 .../wicket/markup/html/link/ResourceLink.java      |  30 +-
 .../apache/wicket/request/resource/IResource.java  |  10 +-
 .../ComponentResourceAuthorizationTest.java        | 492 +++++++++++++++++++++
 .../extensions/ajax/AjaxDownloadBehavior.java      |  24 +-
 .../AjaxDownloadResourceAuthorizationTest.java     | 261 +++++++++++
 .../ExportToolbarResourceAuthorizationTest.java    | 151 +++++++
 10 files changed, 1220 insertions(+), 18 deletions(-)

diff --git 
a/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeResource.java
 
b/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeResource.java
index 9162f51a6b..34cddfe76d 100644
--- 
a/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeResource.java
+++ 
b/wicket-auth-roles/src/main/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AuthorizeResource.java
@@ -26,6 +26,10 @@ import java.lang.annotation.Target;
 /**
  * Annotation for configuring what roles are allowed for requesting the 
annotated resource. It works analogously
  * to {@link 
org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeInstantiation}.
+ * <p>
+ * It applies to every request that serves the annotated resource: one 
requested by the URL of a
+ * mounted or shared {@link 
org.apache.wicket.request.resource.ResourceReference}, and one served
+ * through a component that was handed the resource itself.
  *
  * @author Carl-Eric Menzel
  * @see org.apache.wicket.authorization.IAuthorizationStrategy
diff --git 
a/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AnnotationsRoleResourceTest.java
 
b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AnnotationsRoleResourceTest.java
new file mode 100644
index 0000000000..60b2e39d4f
--- /dev/null
+++ 
b/wicket-auth-roles/src/test/java/org/apache/wicket/authroles/authorization/strategies/role/annotations/AnnotationsRoleResourceTest.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.wicket.authroles.authorization.strategies.role.annotations;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.Serializable;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.wicket.MarkupContainer;
+import 
org.apache.wicket.authroles.authorization.strategies.role.IRoleCheckingStrategy;
+import 
org.apache.wicket.authroles.authorization.strategies.role.RoleAuthorizationStrategy;
+import org.apache.wicket.authroles.authorization.strategies.role.Roles;
+import org.apache.wicket.markup.IMarkupResourceStreamProvider;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.markup.html.image.Image;
+import org.apache.wicket.markup.html.link.ResourceLink;
+import org.apache.wicket.request.resource.IResource;
+import org.apache.wicket.util.resource.IResourceStream;
+import org.apache.wicket.util.resource.StringResourceStream;
+import org.apache.wicket.util.tester.TagTester;
+import org.apache.wicket.util.tester.WicketTester;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * A resource reserved for a role with {@link AuthorizeResource} must not be 
served to a user who
+ * does not hold that role, not even through the component that was handed the 
resource itself.
+ */
+public class AnnotationsRoleResourceTest
+{
+       private WicketTester tester;
+
+       @Before
+       public void setUp()
+       {
+               tester = new WicketTester();
+       }
+
+       @After
+       public void tearDown()
+       {
+               tester.destroy();
+       }
+
+       private void signInAs(String role)
+       {
+               tester.getApplication()
+                       .getSecuritySettings()
+                       .setAuthorizationStrategy(new 
RoleAuthorizationStrategy(new UserRolesAuthorizer(role)));
+       }
+
+       /**
+        * The listener URL Wicket itself rendered for the component with the 
given wicket:id.
+        */
+       private String urlInMarkup(String wicketId, String attribute)
+       {
+               TagTester tag = tester.getTagByWicketId(wicketId);
+               assertNotNull("no tag with wicket:id '" + wicketId + "' was 
rendered", tag);
+
+               String url = tag.getAttribute(attribute);
+               assertNotNull("the tag with wicket:id '" + wicketId + "' has no 
" + attribute, url);
+
+               return url;
+       }
+
+       @Test
+       public void adminMayDownloadTheAdminResourceThroughAResourceLink()
+       {
+               signInAs("ADMIN");
+               TestPage page = new TestPage();
+               tester.startPage(page);
+
+               tester.clickLink("admin", false);
+
+               assertEquals(HttpServletResponse.SC_OK, 
tester.getLastResponse().getStatus());
+               assertTrue("the resource did not stream for an ADMIN", 
page.admin.responded);
+       }
+
+       @Test
+       public void userMayNotDownloadTheAdminResourceThroughAResourceLink()
+       {
+               signInAs("USER");
+               TestPage page = new TestPage();
+               tester.startPage(page);
+
+               tester.clickLink("admin", false);
+
+               assertEquals("the request should have been refused",
+                       HttpServletResponse.SC_FORBIDDEN,
+                       tester.getLastResponse().getStatus());
+               assertFalse("the resource streamed to a user who does not hold 
the ADMIN role",
+                       page.admin.responded);
+       }
+
+       @Test
+       public void userMayNotDownloadTheAdminResourceThroughAnImage()
+       {
+               signInAs("USER");
+               TestPage page = new TestPage();
+               tester.startPage(page);
+
+               tester.executeUrl(urlInMarkup("image", "src"));
+
+               assertEquals("the request should have been refused",
+                       HttpServletResponse.SC_FORBIDDEN,
+                       tester.getLastResponse().getStatus());
+               assertFalse("the resource streamed to a user who does not hold 
the ADMIN role",
+                       page.image.responded);
+       }
+
+       /**
+        * Control: a resource that carries no annotation is not restricted.
+        */
+       @Test
+       public void userMayStillDownloadAnUnannotatedResource()
+       {
+               signInAs("USER");
+               TestPage page = new TestPage();
+               tester.startPage(page);
+
+               tester.clickLink("open", false);
+
+               assertEquals(HttpServletResponse.SC_OK, 
tester.getLastResponse().getStatus());
+               assertTrue("an unannotated resource must still be served", 
page.open.responded);
+       }
+
+       /** A page anyone may open, carrying resources reserved for ADMIN. */
+       public static class TestPage extends WebPage implements 
IMarkupResourceStreamProvider
+       {
+               private static final long serialVersionUID = 1L;
+
+               final AdminResource admin = new AdminResource();
+
+               final AdminResource image = new AdminResource();
+
+               final OpenResource open = new OpenResource();
+
+               TestPage()
+               {
+                       setStatelessHint(false);
+
+                       add(new ResourceLink<Void>("admin", admin));
+                       add(new ResourceLink<Void>("open", open));
+                       add(new Image("image", image));
+               }
+
+               @Override
+               public IResourceStream getMarkupResourceStream(MarkupContainer 
container,
+                       Class<?> containerClass)
+               {
+                       return new StringResourceStream("<html><body>" //
+                               + "<a wicket:id=\"admin\">admin</a>" //
+                               + "<a wicket:id=\"open\">open</a>" //
+                               + "<img wicket:id=\"image\"/>" //
+                               + "</body></html>");
+               }
+       }
+
+       /** Records that it was asked to respond. The annotation cannot go on 
an anonymous class. */
+       private static class RecordingResource implements IResource
+       {
+               private static final long serialVersionUID = 1L;
+
+               boolean responded;
+
+               @Override
+               public void respond(Attributes attributes)
+               {
+                       responded = true;
+                       attributes.getResponse().write("streamed");
+               }
+       }
+
+       @AuthorizeResource("ADMIN")
+       private static class AdminResource extends RecordingResource
+       {
+               private static final long serialVersionUID = 1L;
+       }
+
+       private static class OpenResource extends RecordingResource
+       {
+               private static final long serialVersionUID = 1L;
+       }
+
+       private static final class UserRolesAuthorizer implements 
IRoleCheckingStrategy, Serializable
+       {
+               private static final long serialVersionUID = 1L;
+
+               private final Roles roles;
+
+               UserRolesAuthorizer(String roles)
+               {
+                       this.roles = new Roles(roles);
+               }
+
+               @Override
+               public boolean hasAnyRole(Roles roles)
+               {
+                       return this.roles.hasAnyRole(roles);
+               }
+       }
+}
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/authorization/IAuthorizationStrategy.java
 
b/wicket-core/src/main/java/org/apache/wicket/authorization/IAuthorizationStrategy.java
index 6e37f0f2ac..11159fe98c 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/authorization/IAuthorizationStrategy.java
+++ 
b/wicket-core/src/main/java/org/apache/wicket/authorization/IAuthorizationStrategy.java
@@ -106,11 +106,25 @@ public interface IAuthorizationStrategy
 
        /**
         * Checks whether a request with some parameters is allowed to a 
resource.
+        * <p>
+        * This is asked for every request that serves an {@link IResource}: 
one requested by the URL of
+        * a mounted or shared {@link 
org.apache.wicket.request.resource.ResourceReference}, and one
+        * held by a component or a behavior and served from the listener URL 
of that component - a
+        * {@link org.apache.wicket.markup.html.link.ResourceLink}, an
+        * {@link org.apache.wicket.markup.html.image.Image}, an
+        * {@link org.apache.wicket.markup.html.form.ImageButton}, or the 
AjaxDownloadBehavior of
+        * wicket-extensions.
+        * <p>
+        * Returning {@code false} hands the request to the
+        * {@link IUnauthorizedResourceRequestListener} of the security 
settings, which decides how it
+        * is refused; the default one answers 403.
         *
         * @param resource
         *            The resource that should be processed
         * @param parameters
-        *            The request parameters
+        *            The request parameters, never {@code null}; they are 
empty for a resource served
+        *            through the component that holds it, because a listener 
URL carries no resource
+        *            parameters
         * @return {@code true} if the request to this resource is allowed.
         */
        boolean isResourceAuthorized(IResource resource, PageParameters 
parameters);
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/image/resource/LocalizedImageResource.java
 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/image/resource/LocalizedImageResource.java
index a76a22ef66..e3a14a47aa 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/image/resource/LocalizedImageResource.java
+++ 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/image/resource/LocalizedImageResource.java
@@ -26,9 +26,9 @@ import org.apache.wicket.WicketRuntimeException;
 import org.apache.wicket.markup.ComponentTag;
 import org.apache.wicket.markup.html.border.Border;
 import org.apache.wicket.request.cycle.RequestCycle;
+import org.apache.wicket.request.handler.resource.ResourceRequestHandler;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 import org.apache.wicket.request.resource.IResource;
-import org.apache.wicket.request.resource.IResource.Attributes;
 import org.apache.wicket.request.resource.PackageResourceReference;
 import org.apache.wicket.request.resource.ResourceReference;
 import org.apache.wicket.util.io.IClusterable;
@@ -188,16 +188,32 @@ public final class LocalizedImageResource implements 
IClusterable
        }
 
        /**
+        * Responds with the {@link IResource} this instance holds, once the
+        * {@link org.apache.wicket.authorization.IAuthorizationStrategy} has 
authorized it.
+        * <p>
+        * This is the only place where a resource served through the component 
that owns this instance
+        * is authorized. {@link #setSrcAttribute(ComponentTag)} renders the 
listener URL of that
+        * component whenever a resource - rather than a {@link 
ResourceReference} - is set, so the
+        * request is not carried by a
+        * {@link 
org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler} and 
is
+        * authorized here instead. Every caller arrives here: the src of an
+        * {@link org.apache.wicket.markup.html.image.Image}, each entry of its 
srcset, and the image of
+        * an {@link org.apache.wicket.markup.html.form.ImageButton}.
+        * <p>
+        * A resource the strategy refuses aborts the request through the
+        * {@link 
org.apache.wicket.authorization.IUnauthorizedResourceRequestListener} of the 
security
+        * settings, so a component that responds with several resources writes 
none of the ones that
+        * follow the refused one.
+        * 
         * @param parameters
-        *            page parameters
+        *            page parameters; the callers within Wicket pass {@code 
null}, because a listener
+        *            URL carries no resource parameters, and the strategy and 
the resource then see the
+        *            empty parameters that {@link ResourceRequestHandler} 
substitutes
         */
        public final void onResourceRequested(PageParameters parameters)
        {
                bind();
-               RequestCycle requestCycle = RequestCycle.get();
-               Attributes attributes = new 
Attributes(requestCycle.getRequest(),
-                       requestCycle.getResponse(), parameters);
-               resource.respond(attributes);
+               new ResourceRequestHandler(resource, 
parameters).respond(RequestCycle.get());
        }
 
        /**
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/link/ResourceLink.java
 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/link/ResourceLink.java
index 303c06782c..1308bb1a4e 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/link/ResourceLink.java
+++ 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/link/ResourceLink.java
@@ -17,11 +17,10 @@
 package org.apache.wicket.markup.html.link;
 
 import org.apache.wicket.IRequestListener;
-import org.apache.wicket.request.cycle.RequestCycle;
 import 
org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler;
+import org.apache.wicket.request.handler.resource.ResourceRequestHandler;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 import org.apache.wicket.request.resource.IResource;
-import org.apache.wicket.request.resource.IResource.Attributes;
 import org.apache.wicket.request.resource.ResourceReference;
 
 /**
@@ -81,6 +80,10 @@ public class ResourceLink<T> extends Link<T> implements 
IRequestListener
 
        /**
         * Constructs a link directly to the provided resource.
+        * <p>
+        * Such a resource is served from the listener URL of this link rather 
than from a resource URL
+        * of its own, and it is the {@link #onRequest()} of this link that has 
the
+        * {@link org.apache.wicket.authorization.IAuthorizationStrategy} 
authorize it on every request.
         * 
         * @param id
         *            See Component
@@ -146,13 +149,26 @@ public class ResourceLink<T> extends Link<T> implements 
IRequestListener
                return urlForListener(resourceParameters);
        }
        
+       /**
+        * Responds with the {@link IResource} this link was constructed with.
+        * <p>
+        * The resource is served through a {@link ResourceRequestHandler}, 
which asks the
+        * {@link org.apache.wicket.authorization.IAuthorizationStrategy} to 
authorize it, exactly as it
+        * is asked for a resource that is requested by its own URL. This is 
the only place where a
+        * resource held by this link is authorized, because {@link #getURL()} 
renders the listener URL
+        * of this link for such a resource rather than a resource URL, so the 
request never reaches a
+        * {@link ResourceReferenceRequestHandler}.
+        * <p>
+        * A resource the strategy refuses aborts the request - the
+        * {@link 
org.apache.wicket.authorization.IUnauthorizedResourceRequestListener} of the 
security
+        * settings decides how, the default one answers 403 - so {@link 
#onClick()} is not reached
+        * either. This method is final, so a subclass cannot serve the 
resource without that decision.
+        */
        @Override
        public final void onRequest()
        {
-               Attributes a = new Attributes(RequestCycle.get().getRequest(), 
RequestCycle.get()
-                       .getResponse(), null);
-               resource.respond(a);
-               
+               new ResourceRequestHandler(resource, 
resourceParameters).respond(getRequestCycle());
+
                super.onRequest();
-       }       
+       }
 }
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/request/resource/IResource.java 
b/wicket-core/src/main/java/org/apache/wicket/request/resource/IResource.java
index 82078d154f..32d40305dd 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/request/resource/IResource.java
+++ 
b/wicket-core/src/main/java/org/apache/wicket/request/resource/IResource.java
@@ -33,7 +33,9 @@ public interface IResource extends IClusterable
 {
        /**
         * Attributes that are provided to resource in the {@link 
IResource#respond(Attributes)} method.
-        * Attributes are set by the {@link ResourceRequestHandler}.
+        * Attributes are set by the {@link ResourceRequestHandler}. 
Constructing them in application
+        * code in order to call {@link IResource#respond(Attributes)} serves 
the resource without asking
+        * whether it may be served - see that method.
         * 
         * @author Matej Knopp
         */
@@ -109,6 +111,12 @@ public interface IResource extends IClusterable
 
        /**
         * Renders this resource to response using the provided attributes.
+        * <p>
+        * <b>The resource is written to the response as is, without asking 
whether it may be served.</b>
+        * The {@link org.apache.wicket.authorization.IAuthorizationStrategy} 
is asked in the constructor
+        * of {@link ResourceRequestHandler} and nowhere else, so application 
code that serves a resource
+        * must construct that handler for it and respond or schedule it, 
rather than call this method.
+        * </p>
         * 
         * @param attributes
         */
diff --git 
a/wicket-core/src/test/java/org/apache/wicket/request/handler/resource/ComponentResourceAuthorizationTest.java
 
b/wicket-core/src/test/java/org/apache/wicket/request/handler/resource/ComponentResourceAuthorizationTest.java
new file mode 100644
index 0000000000..47ccaf0f4a
--- /dev/null
+++ 
b/wicket-core/src/test/java/org/apache/wicket/request/handler/resource/ComponentResourceAuthorizationTest.java
@@ -0,0 +1,492 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.wicket.request.handler.resource;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.authorization.IAuthorizationStrategy;
+import org.apache.wicket.markup.IMarkupResourceStreamProvider;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.ImageButton;
+import org.apache.wicket.markup.html.image.Image;
+import org.apache.wicket.markup.html.link.ResourceLink;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.request.resource.IResource;
+import org.apache.wicket.request.resource.ResourceReference;
+import org.apache.wicket.util.lang.Exceptions;
+import org.apache.wicket.util.resource.IResourceStream;
+import org.apache.wicket.util.resource.StringResourceStream;
+import org.apache.wicket.util.tester.TagTester;
+import org.apache.wicket.util.tester.WicketTestCase;
+import org.junit.Test;
+
+/**
+ * A resource a component was handed as an {@link IResource} is served from 
the listener URL of that
+ * component rather than from a resource URL of its own, and the {@link 
IAuthorizationStrategy} has
+ * to authorize it there just as it does when the same instance is requested 
through a
+ * {@link ResourceReference}.
+ * <p>
+ * {@link ResourceAuthorizationTest} covers the decision itself on the handler 
that makes it; this
+ * covers the components that have to reach that handler.
+ */
+public class ComponentResourceAuthorizationTest extends WicketTestCase
+{
+       /** Every resource the strategy was asked about, in the order it was 
asked. */
+       private final List<IResource> asked = new ArrayList<>();
+
+       /** The parameters of the last question put to the strategy. */
+       private PageParameters askedWith;
+
+       /**
+        * Refuses the given resources, allows every other one, and records 
every question asked.
+        */
+       private void deny(final IResource... denied)
+       {
+               final List<IResource> refused = Arrays.asList(denied);
+
+               tester.getApplication()
+                       .getSecuritySettings()
+                       .setAuthorizationStrategy(new 
IAuthorizationStrategy.AllowAllAuthorizationStrategy()
+                       {
+                               @Override
+                               public boolean isResourceAuthorized(IResource 
resource, PageParameters parameters)
+                               {
+                                       asked.add(resource);
+                                       askedWith = parameters;
+                                       return refused.contains(resource) == 
false;
+                               }
+                       });
+       }
+
+       /**
+        * The URL Wicket itself rendered for the component with the given 
wicket:id, which for a
+        * component holding an {@link IResource} is the listener URL of that 
component. It carries the
+        * identifier of the page that rendered it, so the request reaches that 
very instance and the
+        * listener gate of the component is in full effect.
+        */
+       private String urlInMarkup(String wicketId, String attribute)
+       {
+               TagTester tag = tester.getTagByWicketId(wicketId);
+               assertNotNull("no tag with wicket:id '" + wicketId + "' was 
rendered", tag);
+
+               String url = tag.getAttribute(attribute);
+               assertNotNull("the tag with wicket:id '" + wicketId + "' has no 
" + attribute, url);
+               assertTrue("expected a listener URL carrying the page 
identifier, was " + url,
+                       url.contains("?" + 
tester.getLastRenderedPage().getPageId() + "-"));
+
+               return url;
+       }
+
+       private void assertRefused(RecordingResource... resources)
+       {
+               assertEquals("the request should have been refused",
+                       HttpServletResponse.SC_FORBIDDEN,
+                       tester.getLastResponse().getStatus());
+
+               for (RecordingResource resource : resources)
+               {
+                       assertFalse("resource '" + resource + "' streamed 
although the strategy refused it",
+                               resource.responded);
+                       assertFalse("the content of resource '" + resource + "' 
reached the response",
+                               
tester.getLastResponseAsString().contains(resource.name));
+               }
+       }
+
+       private void assertRefusalMessage(RecordingResource resource)
+       {
+               assertEquals("The request to resource '" + resource
+                       + "' with parameters '' cannot be authorized.",
+                       tester.getLastResponse().getErrorMessage());
+       }
+
+       private void assertStreamed(String content, RecordingResource resource)
+       {
+               assertEquals(HttpServletResponse.SC_OK, 
tester.getLastResponse().getStatus());
+               assertTrue("resource '" + resource + "' did not stream", 
resource.responded);
+               assertEquals(content, tester.getLastResponseAsString());
+       }
+
+       @Test
+       public void resourceLinkWithDeniedResourceIsRefused()
+       {
+               ResourceLinkPage page = new ResourceLinkPage();
+               deny(page.denied);
+               tester.startPage(page);
+               assertFalse(page.isPageStateless());
+
+               tester.clickLink("denied", false);
+
+               assertRefused(page.denied);
+               assertRefusalMessage(page.denied);
+               assertFalse("ResourceLink#onClick() ran although the resource 
request was refused",
+                       page.deniedClicked);
+       }
+
+       /**
+        * Control: the fix must not refuse a resource the strategy allows.
+        */
+       @Test
+       public void resourceLinkWithAllowedResourceIsStillServed()
+       {
+               ResourceLinkPage page = new ResourceLinkPage();
+               deny(page.denied);
+               tester.startPage(page);
+
+               tester.clickLink("allowed", false);
+
+               assertStreamed("allowed", page.allowed);
+       }
+
+       @Test
+       public void resourceLinkWithDeniedResourceReferenceIsStillRefused()
+       {
+               ResourceLinkPage page = new ResourceLinkPage();
+               deny(page.referenced);
+               tester.startPage(page);
+
+               tester.clickLink("reference", false);
+
+               assertRefused(page.referenced);
+       }
+
+       /**
+        * Control: the other constructor of the same component renders a 
resource URL, which is the
+        * route that was authorized all along.
+        */
+       @Test
+       public void resourceLinkWithAllowedResourceReferenceIsStillServed()
+       {
+               ResourceLinkPage page = new ResourceLinkPage();
+               deny(page.denied);
+               tester.startPage(page);
+
+               String href = 
tester.getTagByWicketId("reference").getAttribute("href");
+               assertTrue("expected a resource URL, was " + href, 
href.contains("resource/"));
+
+               tester.clickLink("reference", false);
+
+               assertStreamed("referenced", page.referenced);
+       }
+
+       @Test
+       public void imageWithDeniedResourceIsRefused()
+       {
+               ImagePage page = new ImagePage();
+               deny(page.src);
+               tester.startPage(page);
+
+               tester.executeUrl(urlInMarkup("src", "src"));
+
+               assertRefused(page.src);
+               assertRefusalMessage(page.src);
+       }
+
+       /**
+        * Control.
+        */
+       @Test
+       public void imageWithAllowedResourceIsStillServed()
+       {
+               ImagePage page = new ImagePage();
+               deny(page.fromModel);
+               tester.startPage(page);
+
+               tester.executeUrl(urlInMarkup("src", "src"));
+
+               assertStreamed("src", page.src);
+       }
+
+       @Test
+       public void imageWithDeniedResourceFromItsModelIsRefused()
+       {
+               ImagePage page = new ImagePage();
+               deny(page.fromModel);
+               tester.startPage(page);
+
+               tester.executeUrl(urlInMarkup("model", "src"));
+
+               assertRefused(page.fromModel);
+       }
+
+       @Test
+       public void deniedSrcSetEntryOfAnImageIsRefused()
+       {
+               ImagePage page = new ImagePage();
+               deny(page.setEntry);
+               tester.startPage(page);
+
+               tester.executeUrl(urlInMarkup("srcset", "src"));
+
+               assertEquals("the request should have been refused",
+                       HttpServletResponse.SC_FORBIDDEN,
+                       tester.getLastResponse().getStatus());
+               assertFalse("the refused srcset entry streamed", 
page.setEntry.responded);
+       }
+
+       /**
+        * Control.
+        */
+       @Test
+       public void allowedSrcSetOfAnImageIsStillServed()
+       {
+               ImagePage page = new ImagePage();
+               deny(page.src);
+               tester.startPage(page);
+
+               tester.executeUrl(urlInMarkup("srcset", "src"));
+
+               assertEquals(HttpServletResponse.SC_OK, 
tester.getLastResponse().getStatus());
+               assertTrue("the src of the image did not stream", 
page.setSrc.responded);
+               assertTrue("the srcset entry of the image did not stream", 
page.setEntry.responded);
+       }
+
+       @Test
+       public void theStrategyIsAskedAboutEverySrcSetEntry()
+       {
+               ImagePage page = new ImagePage();
+               deny();
+               tester.startPage(page);
+
+               tester.executeUrl(urlInMarkup("srcset", "src"));
+
+               assertEquals("the strategy must be asked about the src and 
about every srcset entry",
+                       Arrays.asList(page.setSrc, page.setEntry),
+                       asked);
+       }
+
+       @Test
+       public void imageButtonWithDeniedResourceIsRefused()
+       {
+               ImageButtonPage page = new ImageButtonPage();
+               deny(page.denied);
+               tester.startPage(page);
+
+               tester.executeUrl(urlInMarkup("denied", "src"));
+
+               assertRefused(page.denied);
+               assertRefusalMessage(page.denied);
+       }
+
+       /**
+        * Control.
+        */
+       @Test
+       public void imageButtonWithAllowedResourceIsStillServed()
+       {
+               ImageButtonPage page = new ImageButtonPage();
+               deny(page.denied);
+               tester.startPage(page);
+
+               tester.executeUrl(urlInMarkup("allowed", "src"));
+
+               assertStreamed("buttonAllowed", page.allowed);
+       }
+
+       @Test
+       public void 
theStrategyIsAskedAboutTheInstanceTheComponentHoldsWithEmptyParameters()
+       {
+               ResourceLinkPage page = new ResourceLinkPage();
+               deny();
+               tester.startPage(page);
+
+               tester.clickLink("allowed", false);
+
+               assertEquals(Arrays.asList(page.allowed), asked);
+               assertNotNull("the strategy must not be asked with null 
parameters", askedWith);
+               assertTrue("a listener URL carries no resource parameters", 
askedWith.isEmpty());
+       }
+
+       @Test
+       public void 
theUnauthorizedResourceRequestListenerOfTheApplicationIsUsed()
+       {
+               ResourceLinkPage page = new ResourceLinkPage();
+               deny(page.denied);
+               tester.getApplication()
+                       .getSecuritySettings()
+                       .setUnauthorizedResourceRequestListener((resource, 
parameters) -> {
+                               throw new IllegalStateException("refused " + 
resource);
+                       });
+               tester.startPage(page);
+
+               try
+               {
+                       tester.clickLink("denied", false);
+                       fail("the listener should have refused the request");
+               }
+               catch (RuntimeException e)
+               {
+                       assertEquals("refused denied",
+                               Exceptions.findCause(e, 
IllegalStateException.class).getMessage());
+               }
+
+               assertFalse("the resource streamed although the listener 
refused the request",
+                       page.denied.responded);
+       }
+
+       /** A stateful page carrying both constructors of {@link ResourceLink}. 
*/
+       public static class ResourceLinkPage extends WebPage implements 
IMarkupResourceStreamProvider
+       {
+               private static final long serialVersionUID = 1L;
+
+               final RecordingResource denied = new 
RecordingResource("denied");
+
+               final RecordingResource allowed = new 
RecordingResource("allowed");
+
+               final RecordingResource referenced = new 
RecordingResource("referenced");
+
+               boolean deniedClicked;
+
+               ResourceLinkPage()
+               {
+                       setStatelessHint(false);
+
+                       add(new ResourceLink<Void>("denied", denied)
+                       {
+                               private static final long serialVersionUID = 1L;
+
+                               @Override
+                               public void onClick()
+                               {
+                                       deniedClicked = true;
+                               }
+                       });
+                       add(new ResourceLink<Void>("allowed", allowed));
+                       add(new ResourceLink<Void>("reference",
+                               new ResourceReference(ResourceLinkPage.class, 
"referenced")
+                               {
+                                       private static final long 
serialVersionUID = 1L;
+
+                                       @Override
+                                       public IResource getResource()
+                                       {
+                                               return referenced;
+                                       }
+                               }));
+               }
+
+               @Override
+               public IResourceStream getMarkupResourceStream(MarkupContainer 
container,
+                       Class<?> containerClass)
+               {
+                       return new StringResourceStream("<html><body>" //
+                               + "<a wicket:id=\"denied\">denied</a>" //
+                               + "<a wicket:id=\"allowed\">allowed</a>" //
+                               + "<a wicket:id=\"reference\">reference</a>" //
+                               + "</body></html>");
+               }
+       }
+
+       /** A stateful page whose images all hold an {@link IResource} rather 
than a reference. */
+       public static class ImagePage extends WebPage implements 
IMarkupResourceStreamProvider
+       {
+               private static final long serialVersionUID = 1L;
+
+               final RecordingResource src = new RecordingResource("src");
+
+               final RecordingResource setSrc = new 
RecordingResource("setSrc");
+
+               final RecordingResource setEntry = new 
RecordingResource("setEntry");
+
+               final RecordingResource fromModel = new 
RecordingResource("fromModel");
+
+               ImagePage()
+               {
+                       setStatelessHint(false);
+
+                       add(new Image("src", src));
+                       add(new Image("srcset", setSrc, setEntry));
+                       add(new Image("model", Model.of(fromModel)));
+               }
+
+               @Override
+               public IResourceStream getMarkupResourceStream(MarkupContainer 
container,
+                       Class<?> containerClass)
+               {
+                       return new StringResourceStream("<html><body>" //
+                               + "<img wicket:id=\"src\"/>" //
+                               + "<img wicket:id=\"srcset\"/>" //
+                               + "<img wicket:id=\"model\"/>" //
+                               + "</body></html>");
+               }
+       }
+
+       /** A stateful page carrying two image buttons, each holding an {@link 
IResource}. */
+       public static class ImageButtonPage extends WebPage implements 
IMarkupResourceStreamProvider
+       {
+               private static final long serialVersionUID = 1L;
+
+               final RecordingResource denied = new 
RecordingResource("buttonDenied");
+
+               final RecordingResource allowed = new 
RecordingResource("buttonAllowed");
+
+               ImageButtonPage()
+               {
+                       setStatelessHint(false);
+
+                       Form<Void> form = new Form<>("form");
+                       add(form);
+                       form.add(new ImageButton("denied", denied));
+                       form.add(new ImageButton("allowed", allowed));
+               }
+
+               @Override
+               public IResourceStream getMarkupResourceStream(MarkupContainer 
container,
+                       Class<?> containerClass)
+               {
+                       return new StringResourceStream("<html><body>" //
+                               + "<form wicket:id=\"form\">" //
+                               + "<input type=\"image\" 
wicket:id=\"denied\"/>" //
+                               + "<input type=\"image\" 
wicket:id=\"allowed\"/>" //
+                               + "</form></body></html>");
+               }
+       }
+
+       /** Records that it was asked to respond, and writes something 
recognisable when it does. */
+       private static class RecordingResource implements IResource
+       {
+               private static final long serialVersionUID = 1L;
+
+               private final String name;
+
+               private boolean responded;
+
+               RecordingResource(String name)
+               {
+                       this.name = name;
+               }
+
+               @Override
+               public void respond(Attributes attributes)
+               {
+                       responded = true;
+                       attributes.getResponse().write(name);
+               }
+
+               @Override
+               public String toString()
+               {
+                       return name;
+               }
+       }
+}
diff --git 
a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/AjaxDownloadBehavior.java
 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/AjaxDownloadBehavior.java
index 609a2bee1d..521145725c 100644
--- 
a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/AjaxDownloadBehavior.java
+++ 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/AjaxDownloadBehavior.java
@@ -31,6 +31,7 @@ import org.apache.wicket.markup.head.JavaScriptHeaderItem;
 import org.apache.wicket.request.Response;
 import org.apache.wicket.request.cycle.RequestCycle;
 import 
org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler;
+import org.apache.wicket.request.handler.resource.ResourceRequestHandler;
 import org.apache.wicket.request.http.WebResponse;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 import org.apache.wicket.request.resource.ContentDisposition;
@@ -128,6 +129,11 @@ public class AjaxDownloadBehavior extends 
AbstractDefaultAjaxBehavior
 
        /**
         * Download of a {@link IResource}.
+        * <p>
+        * The resource is served from the listener URL of the component this 
behavior is bound to
+        * rather than from a resource URL of its own, and the
+        * {@link org.apache.wicket.authorization.IAuthorizationStrategy} 
authorizes it on every
+        * request, exactly as it does for a {@link ResourceReference}.
         *
         * @param resource
         *            resource to download
@@ -356,16 +362,28 @@ public class AjaxDownloadBehavior extends 
AbstractDefaultAjaxBehavior
                        return false;
                }
 
+               /**
+                * Responds with the resource, once the
+                * {@link 
org.apache.wicket.authorization.IAuthorizationStrategy} has authorized it.
+                * <p>
+                * The handler is created before the cookie is added, because 
it is its constructor that
+                * asks the strategy. That cookie is what tells 
wicket-ajaxdownload.js that the download
+                * finished, it is looked at there before the response body is, 
and a buffered response
+                * replays a cookie ahead of the error the strategy causes - so 
a refused download must not
+                * leave one behind, or it would be reported as
+                * {@link 
AjaxDownloadBehavior#onDownloadSuccess(AjaxRequestTarget)}.
+                */
                @Override
                public void onRequest()
                {
                        final RequestCycle requestCycle = RequestCycle.get();
                        final Response response = requestCycle.getResponse();
-                       ((WebResponse) response).addCookie(cookie(getName()));
 
-                       Attributes a = new 
Attributes(requestCycle.getRequest(), response, null);
+                       ResourceRequestHandler handler = new 
ResourceRequestHandler(resource, null);
+
+                       ((WebResponse) response).addCookie(cookie(getName()));
 
-                       resource.respond(a);
+                       handler.respond(requestCycle);
                }
 
                public CharSequence getUrl()
diff --git 
a/wicket-extensions/src/test/java/org/apache/wicket/extensions/ajax/AjaxDownloadResourceAuthorizationTest.java
 
b/wicket-extensions/src/test/java/org/apache/wicket/extensions/ajax/AjaxDownloadResourceAuthorizationTest.java
new file mode 100644
index 0000000000..3787779a48
--- /dev/null
+++ 
b/wicket-extensions/src/test/java/org/apache/wicket/extensions/ajax/AjaxDownloadResourceAuthorizationTest.java
@@ -0,0 +1,261 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.wicket.extensions.ajax;
+
+import java.util.Arrays;
+import java.util.List;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.Page;
+import org.apache.wicket.authorization.IAuthorizationStrategy;
+import org.apache.wicket.behavior.Behavior;
+import org.apache.wicket.core.request.handler.ListenerRequestHandler;
+import org.apache.wicket.core.request.handler.PageAndComponentProvider;
+import org.apache.wicket.markup.IMarkupResourceStreamProvider;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.request.IRequestHandler;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.request.resource.IResource;
+import org.apache.wicket.request.resource.ResourceReference;
+import org.apache.wicket.util.resource.IResourceStream;
+import org.apache.wicket.util.resource.StringResourceStream;
+import org.apache.wicket.util.tester.WicketTestCase;
+import org.junit.Test;
+
+/**
+ * An {@link AjaxDownloadBehavior} constructed from an {@link IResource} 
serves it from the listener
+ * URL of the component it is bound to, so the {@link IAuthorizationStrategy} 
has to authorize it
+ * there just as it does for the {@link ResourceReference} the other 
constructor takes.
+ */
+public class AjaxDownloadResourceAuthorizationTest extends WicketTestCase
+{
+       private void deny(final IResource... denied)
+       {
+               final List<IResource> refused = Arrays.asList(denied);
+
+               tester.getApplication()
+                       .getSecuritySettings()
+                       .setAuthorizationStrategy(new 
IAuthorizationStrategy.AllowAllAuthorizationStrategy()
+                       {
+                               @Override
+                               public boolean isResourceAuthorized(IResource 
resource, PageParameters parameters)
+                               {
+                                       return refused.contains(resource) == 
false;
+                               }
+                       });
+       }
+
+       /**
+        * The URL a behavior constructed from an {@link IResource} hands to 
the browser when it
+        * initiates: the listener URL of the behavior the download added to 
serve that resource. That
+        * behavior is private, so it can only be recognised as the one 
behavior of the component that
+        * is not the download itself.
+        */
+       private String downloadUrl(Component component, AjaxDownloadBehavior 
download)
+       {
+               Page page = component.getPage();
+               assertFalse("the fixture page must be stateful", 
page.isPageStateless());
+
+               Behavior resourceBehavior = null;
+               for (Behavior behavior : component.getBehaviors())
+               {
+                       if (behavior != download)
+                       {
+                               resourceBehavior = behavior;
+                       }
+               }
+               assertNotNull("the download did not add a behavior to serve its 
resource", resourceBehavior);
+
+               IRequestHandler handler = new ListenerRequestHandler(
+                       new PageAndComponentProvider(page, component),
+                       component.getBehaviorId(resourceBehavior));
+
+               String url = tester.urlFor(handler).toString();
+               assertTrue("expected a listener URL carrying the page 
identifier, was " + url,
+                       url.contains("?" + page.getPageId() + "-"));
+
+               return url;
+       }
+
+       @Test
+       public void downloadOfADeniedResourceIsRefused()
+       {
+               DownloadPage page = new DownloadPage();
+               deny(page.resource);
+               tester.startPage(page);
+
+               tester.executeUrl(downloadUrl(page.resourceContainer, 
page.resourceDownload));
+
+               assertEquals("the download should have been refused",
+                       HttpServletResponse.SC_FORBIDDEN,
+                       tester.getLastResponse().getStatus());
+               assertEquals(
+                       "The request to resource 'streamedDownload' with 
parameters '' cannot be authorized.",
+                       tester.getLastResponse().getErrorMessage());
+               assertFalse("the resource streamed although the strategy 
refused it",
+                       page.resource.responded);
+               assertFalse("the content of the resource reached the response",
+                       
tester.getLastResponseAsString().contains("streamedDownload"));
+       }
+
+       /**
+        * Control: the fix must not refuse a download the strategy allows.
+        */
+       @Test
+       public void downloadOfAnAllowedResourceIsStillServed()
+       {
+               DownloadPage page = new DownloadPage();
+               deny(page.referenced);
+               tester.startPage(page);
+
+               tester.executeUrl(downloadUrl(page.resourceContainer, 
page.resourceDownload));
+
+               assertEquals(HttpServletResponse.SC_OK, 
tester.getLastResponse().getStatus());
+               assertTrue("the resource did not stream", 
page.resource.responded);
+               assertEquals("streamedDownload", 
tester.getLastResponseAsString());
+       }
+
+       /**
+        * The {@link ResourceReference} constructor serves its resource from a 
resource URL instead, so
+        * it adds no behavior of its own - which is why that route was 
authorized all along.
+        */
+       @Test
+       public void theResourceReferenceVariantAddsNoBehaviorOfItsOwn()
+       {
+               DownloadPage page = new DownloadPage();
+               tester.startPage(page);
+
+               assertEquals("the ResourceReference variant must not add a 
behavior that serves the resource",
+                       1,
+                       page.referenceContainer.getBehaviors().size());
+       }
+
+       @Test
+       public void downloadOfADeniedResourceReferenceIsStillRefused()
+       {
+               DownloadPage page = new DownloadPage();
+               deny(page.referenced);
+               tester.startPage(page);
+
+               tester.startResourceReference(page.reference);
+
+               assertEquals(HttpServletResponse.SC_FORBIDDEN, 
tester.getLastResponse().getStatus());
+               assertFalse("the referenced resource streamed although the 
strategy refused it",
+                       page.referenced.responded);
+       }
+
+       /**
+        * Control.
+        */
+       @Test
+       public void downloadOfAnAllowedResourceReferenceIsStillServed()
+       {
+               DownloadPage page = new DownloadPage();
+               deny(page.resource);
+               tester.startPage(page);
+
+               tester.startResourceReference(page.reference);
+
+               assertEquals(HttpServletResponse.SC_OK, 
tester.getLastResponse().getStatus());
+               assertTrue("the referenced resource did not stream", 
page.referenced.responded);
+       }
+
+       /** A stateful page carrying both constructors of {@link 
AjaxDownloadBehavior}. */
+       public static class DownloadPage extends WebPage implements 
IMarkupResourceStreamProvider
+       {
+               private static final long serialVersionUID = 1L;
+
+               final RecordingResource resource = new 
RecordingResource("streamedDownload");
+
+               final RecordingResource referenced = new 
RecordingResource("referenced");
+
+               final ResourceReference reference;
+
+               final WebMarkupContainer resourceContainer;
+
+               final WebMarkupContainer referenceContainer;
+
+               final AjaxDownloadBehavior resourceDownload;
+
+               DownloadPage()
+               {
+                       setStatelessHint(false);
+
+                       reference = new ResourceReference(DownloadPage.class, 
"referenced")
+                       {
+                               private static final long serialVersionUID = 1L;
+
+                               @Override
+                               public IResource getResource()
+                               {
+                                       return referenced;
+                               }
+                       };
+
+                       resourceDownload = new AjaxDownloadBehavior(resource);
+                       resourceContainer = new WebMarkupContainer("resource");
+                       resourceContainer.add(resourceDownload);
+                       add(resourceContainer);
+
+                       referenceContainer = new 
WebMarkupContainer("reference");
+                       referenceContainer.add(new 
AjaxDownloadBehavior(reference));
+                       add(referenceContainer);
+               }
+
+               @Override
+               public IResourceStream getMarkupResourceStream(MarkupContainer 
container,
+                       Class<?> containerClass)
+               {
+                       return new StringResourceStream("<html><body>" //
+                               + "<span 
wicket:id=\"resource\">resource</span>" //
+                               + "<span 
wicket:id=\"reference\">reference</span>" //
+                               + "</body></html>");
+               }
+       }
+
+       /** Records that it was asked to respond, and writes something 
recognisable when it does. */
+       private static class RecordingResource implements IResource
+       {
+               private static final long serialVersionUID = 1L;
+
+               private final String name;
+
+               private boolean responded;
+
+               RecordingResource(String name)
+               {
+                       this.name = name;
+               }
+
+               @Override
+               public void respond(Attributes attributes)
+               {
+                       responded = true;
+                       attributes.getResponse().write(name);
+               }
+
+               @Override
+               public String toString()
+               {
+                       return name;
+               }
+       }
+}
diff --git 
a/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/repeater/data/table/export/ExportToolbarResourceAuthorizationTest.java
 
b/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/repeater/data/table/export/ExportToolbarResourceAuthorizationTest.java
new file mode 100644
index 0000000000..0869974153
--- /dev/null
+++ 
b/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/repeater/data/table/export/ExportToolbarResourceAuthorizationTest.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.wicket.extensions.markup.html.repeater.data.table.export;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.authorization.IAuthorizationStrategy;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.Contact;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.DataTable;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
+import 
org.apache.wicket.extensions.markup.html.repeater.data.table.PropertyColumn;
+import org.apache.wicket.markup.IMarkupResourceStreamProvider;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.markup.html.link.ResourceLink;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.request.resource.IResource;
+import org.apache.wicket.request.resource.ResourceStreamResource;
+import org.apache.wicket.util.resource.IResourceStream;
+import org.apache.wicket.util.resource.StringResourceStream;
+import org.apache.wicket.util.tester.WicketTestCase;
+import org.apache.wicket.util.visit.IVisit;
+import org.apache.wicket.util.visit.IVisitor;
+import org.junit.Test;
+
+/**
+ * {@link ExportToolbar} builds each of its export links from an {@link 
IResource} of its own, so
+ * the exported contents of a data table are subject to the {@link 
IAuthorizationStrategy} like any
+ * other resource a component holds.
+ */
+public class ExportToolbarResourceAuthorizationTest extends WicketTestCase
+{
+       /** Every resource the strategy was asked about, in the order it was 
asked. */
+       private final List<IResource> asked = new ArrayList<>();
+
+       private void allowEverything(final boolean allowed)
+       {
+               tester.getApplication()
+                       .getSecuritySettings()
+                       .setAuthorizationStrategy(new 
IAuthorizationStrategy.AllowAllAuthorizationStrategy()
+                       {
+                               @Override
+                               public boolean isResourceAuthorized(IResource 
resource, PageParameters parameters)
+                               {
+                                       asked.add(resource);
+                                       return allowed;
+                               }
+                       });
+       }
+
+       /**
+        * The toolbar creates its resource and its link itself, so neither can 
be named from here.
+        */
+       private ResourceLink<?> exportLink()
+       {
+               ResourceLink<?> link = tester.getLastRenderedPage()
+                       .visitChildren(ResourceLink.class, new 
IVisitor<ResourceLink<?>, ResourceLink<?>>()
+                       {
+                               @Override
+                               public void component(ResourceLink<?> link, 
IVisit<ResourceLink<?>> visit)
+                               {
+                                       visit.stop(link);
+                               }
+                       });
+
+               assertNotNull("the export toolbar did not render an export 
link", link);
+               return link;
+       }
+
+       @Test
+       public void exportIsRefusedWhenTheStrategyRefusesTheResource()
+       {
+               tester.startPage(new ExportPage());
+               allowEverything(false);
+
+               tester.clickLink(exportLink().getPageRelativePath(), false);
+
+               assertEquals("the export should have been refused",
+                       HttpServletResponse.SC_FORBIDDEN,
+                       tester.getLastResponse().getStatus());
+               assertFalse("the exported data reached the response although 
the strategy refused it",
+                       
tester.getLastResponseAsString().contains("clandestine"));
+               assertEquals("the strategy should have been asked once", 1, 
asked.size());
+               assertTrue("expected the resource the toolbar builds over the 
exported data, was " + asked.get(0),
+                       asked.get(0) instanceof ResourceStreamResource);
+       }
+
+       /**
+        * Control: the fix must not refuse an export the strategy allows.
+        */
+       @Test
+       public void exportIsStillServedWhenTheStrategyAllowsTheResource()
+       {
+               tester.startPage(new ExportPage());
+               allowEverything(true);
+
+               tester.clickLink(exportLink().getPageRelativePath(), false);
+
+               assertEquals(HttpServletResponse.SC_OK, 
tester.getLastResponse().getStatus());
+               assertTrue("the export did not contain the data of the table",
+                       
tester.getLastResponseAsString().contains("clandestine"));
+       }
+
+       /** A table of one row with one exportable column, and a toolbar that 
exports it. */
+       public static class ExportPage extends WebPage implements 
IMarkupResourceStreamProvider
+       {
+               private static final long serialVersionUID = 1L;
+
+               ExportPage()
+               {
+                       setStatelessHint(false);
+
+                       List<IColumn<Contact, String>> columns = new 
ArrayList<>();
+                       columns.add(new PropertyColumn<Contact, 
String>(Model.of("firstName"), "firstName"));
+
+                       DataTable<Contact, String> table = new 
DataTable<>("table", columns,
+                               new ListDataProvider<>(Arrays.asList(new 
Contact("clandestine", "row"))), 10);
+                       table.addBottomToolbar(new ExportToolbar(table, 
Model.of("Export to"),
+                               Model.of("export")).addDataExporter(new 
CSVDataExporter()));
+                       add(table);
+               }
+
+               @Override
+               public IResourceStream getMarkupResourceStream(MarkupContainer 
container,
+                       Class<?> containerClass)
+               {
+                       return new StringResourceStream(
+                               "<html><body><table 
wicket:id=\"table\"></table></body></html>");
+               }
+       }
+}

Reply via email to