CAUSEWAY-3676: adds config properties to control whether/how resources can be downloaded

Also uses spring.graphql.path as the location of the resource controller
diff --git a/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java b/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
index 036f2ba..067b072 100644
--- a/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
+++ b/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
@@ -2433,6 +2433,55 @@
                 private String zonedDateTimeFormat = "yyyy-MM-dd'T'HH:mm:ssXXX";
             }
 
+            /**
+             * The different ways in which resources ({@link org.apache.causeway.applib.value.Blob} bytes,
+             * {@link org.apache.causeway.applib.value.Clob} chars, grids and icons) can be downloaded from the
+             * resource controller.
+             */
+            public enum ResponseType {
+                /**
+                 * Do not allow the resources to be downloaded at all.  This is the default.
+                 *
+                 * <p>
+                 *     In this case any {@link org.apache.causeway.applib.value.Blob} and
+                 *     {@link org.apache.causeway.applib.value.Clob} properties will <i>not</i> provide a link to
+                 *     the URL.  Attempting to download from the resource controller will result in a 403 (forbidden).
+                 * </p>
+                 */
+                FORBIDDEN,
+                /**
+                 * Allows resources to be downloaded directly.
+                 *
+                 * <p>
+                 *     <b>IMPORTANT: </b> if enabling this configuration property, make sure that the <code>ResourcesController</code> endpoints
+                 *     are secured appropriately.
+                 * </p>
+                 */
+                DIRECT,
+                /**
+                 * Allows resources to be downloaded as attachments (using <code>Content-Disposition</code> header).
+                 *
+                 * <p>
+                 *     <b>IMPORTANT: </b> if enabling this configuration property, make sure that the <code>ResourcesController</code> endpoints
+                 *     are secured appropriately.
+                 * </p>
+                 */
+                ATTACHMENT,
+                ;
+            }
+
+            @Getter
+            private final Resources resources = new Resources();
+            @Data
+            public static class Resources {
+                /**
+                 * How resources ({@link org.apache.causeway.applib.value.Blob} bytes,
+                 * {@link org.apache.causeway.applib.value.Clob} chars, grids and icons) can be downloaded from the
+                 * resource controller.
+                 */
+                private ResponseType responseType = ResponseType.FORBIDDEN;
+            }
+
             @Getter
             private final Authentication authentication = new Authentication();
             @Data
@@ -2454,7 +2503,6 @@
                     private List<String> roles;
                 }
             }
-
         }
 
         private final Restfulobjects restfulobjects = new Restfulobjects();
diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMeta.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMeta.java
index 7249f31..7c5de51 100644
--- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMeta.java
+++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMeta.java
@@ -25,6 +25,7 @@
 import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.applib.services.bookmark.BookmarkService;
 import org.apache.causeway.applib.services.metamodel.BeanSort;
+import org.apache.causeway.core.config.CausewayConfiguration;
 import org.apache.causeway.core.metamodel.facets.members.cssclass.CssClassFacet;
 import org.apache.causeway.core.metamodel.facets.object.entity.EntityFacet;
 import org.apache.causeway.core.metamodel.facets.object.layout.LayoutFacet;
@@ -34,6 +35,8 @@
 import org.apache.causeway.viewer.graphql.model.context.Context;
 import org.apache.causeway.viewer.graphql.model.mmproviders.ObjectSpecificationProvider;
 
+import org.springframework.beans.factory.annotation.Value;
+
 import lombok.val;
 
 public class GqlvMeta extends GqlvAbstractCustom {
@@ -43,12 +46,14 @@
     private final GqlvMetaLogicalTypeName metaLogicalTypeName;
     private final GqlvMetaVersion metaVersion;
     private final GqlvMetaTitle metaTitle;
-    private final GqlvMetaIcon metaIconName;
+    private final GqlvMetaIcon metaIcon;
     private final GqlvMetaCssClass metaCssClass;
     private final GqlvMetaLayout metaLayout;
     private final GqlvMetaGrid metaGrid;
     private final GqlvMetaSaveAs metaSaveAs;
 
+    private final CausewayConfiguration.Viewer.Graphql graphqlConfiguration;
+
     public GqlvMeta(
             final Holder holder,
             final Context context
@@ -56,12 +61,14 @@
         super(TypeNames.metaTypeNameFor(holder.getObjectSpecification()), context);
         this.holder = holder;
 
+        this.graphqlConfiguration = context.causewayConfiguration.getViewer().getGraphql();
+
         if(isBuilt()) {
             this.metaId = null;
             this.metaLogicalTypeName = null;
             this.metaVersion = null;
             this.metaTitle = null;
-            this.metaIconName = null;
+            this.metaIcon = null;
             this.metaCssClass = null;
             this.metaLayout = null;
             this.metaGrid = null;
@@ -73,16 +80,21 @@
         addChildFieldFor(this.metaLogicalTypeName = new GqlvMetaLogicalTypeName(context));
         addChildFieldFor(this.metaVersion = isEntity() ? new GqlvMetaVersion(context) : null);
         addChildFieldFor(this.metaTitle = new GqlvMetaTitle(context));
-        addChildFieldFor(this.metaIconName = new GqlvMetaIcon(context));
         addChildFieldFor(this.metaCssClass = new GqlvMetaCssClass(context));
         addChildFieldFor(this.metaLayout = new GqlvMetaLayout(context));
-        addChildFieldFor(this.metaGrid = new GqlvMetaGrid(context));
         addChildFieldFor(this.metaSaveAs = new GqlvMetaSaveAs(context));
 
-        val fieldName = context.causewayConfiguration.getViewer().getGraphql().getMetaData().getFieldName();
+        addChildFieldFor(this.metaIcon = isResourceNotForbidden() ? new GqlvMetaIcon(context) : null);
+        addChildFieldFor(this.metaGrid = isResourceNotForbidden() ? new GqlvMetaGrid(context) : null);
+
+        val fieldName = graphqlConfiguration.getMetaData().getFieldName();
         buildObjectTypeAndField(fieldName);
     }
 
+    private boolean isResourceNotForbidden() {
+        return graphqlConfiguration.getResources().getResponseType() != CausewayConfiguration.Viewer.Graphql.ResponseType.FORBIDDEN;
+    }
+
     private boolean isEntity() {
         return holder.getObjectSpecification().getBeanSort() == BeanSort.ENTITY;
     }
@@ -90,6 +102,7 @@
     @Override
     protected void addDataFetchersForChildren() {
         if (metaId == null) {
+            // none of the fields will have been initialized
             return;
         }
         metaId.addDataFetcher(this);
@@ -98,17 +111,21 @@
             metaVersion.addDataFetcher(this);
         }
         metaTitle.addDataFetcher(this);
-        metaIconName.addDataFetcher(this);
         metaCssClass.addDataFetcher(this);
         metaLayout.addDataFetcher(this);
-        metaGrid.addDataFetcher(this);
         metaSaveAs.addDataFetcher(this);
+        if (metaGrid != null) {
+            metaGrid.addDataFetcher(this);
+        }
+        if (metaIcon != null) {
+            metaIcon.addDataFetcher(this);
+        }
     }
 
     @Override
     public Object fetchData(final DataFetchingEnvironment environment) {
         return context.bookmarkService.bookmarkFor(environment.getSource())
-                .map(bookmark -> new Fetcher(bookmark, context.bookmarkService, context.objectManager))
+                .map(bookmark -> new Fetcher(bookmark, context.bookmarkService, context.objectManager, context.causewayConfiguration))
                 .orElseThrow();
     }
 
@@ -121,14 +138,20 @@
         private final Bookmark bookmark;
         private final BookmarkService bookmarkService;
         private final ObjectManager objectManager;
+        private final CausewayConfiguration causewayConfiguration;
+        private final String graphqlPath;
 
         Fetcher(
                 final Bookmark bookmark,
                 final BookmarkService bookmarkService,
-                final ObjectManager objectManager) {
+                final ObjectManager objectManager,
+                final CausewayConfiguration causewayConfiguration
+        ) {
             this.bookmark = bookmark;
             this.bookmarkService = bookmarkService;
             this.objectManager = objectManager;
+            this.causewayConfiguration = causewayConfiguration;
+            this.graphqlPath = causewayConfiguration.valueOf("spring.graphql.path").orElse("/graphql");
         }
 
         public String logicalTypeName(){
@@ -193,8 +216,8 @@
             return managedObject()
                     .flatMap(Bookmarkable::getBookmark
                     ).map(bookmark -> String.format(
-                            "///%s/object/%s:%s/_meta/%s",
-                            "graphql", bookmark.getLogicalTypeName(), bookmark.getIdentifier(), resource) )
+                            "//%s/object/%s:%s/%s/%s",
+                            graphqlPath, bookmark.getLogicalTypeName(), bookmark.getIdentifier(), causewayConfiguration.getViewer().getGraphql().getMetaData().getFieldName(), resource) )
                     .orElse(null);
         }
 
diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlob.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlob.java
index 6380205..794bcca 100644
--- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlob.java
+++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlob.java
@@ -22,6 +22,7 @@
 
 import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition;
 
+import org.apache.causeway.core.config.CausewayConfiguration;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
 import org.apache.causeway.viewer.graphql.model.context.Context;
@@ -39,13 +40,16 @@
     final GqlvPropertyGetBlobMimeType blobMimeType;
     final GqlvPropertyGetBlobName blobBytes;
 
+    private final CausewayConfiguration.Viewer.Graphql graphqlConfiguration;
+
     public GqlvPropertyGetBlob(
             final Holder holder,
             final Context context) {
         super(TypeNames.propertyBlobTypeNameFor(holder.getObjectSpecification(), holder.getObjectMember()), context);
-
         this.holder = holder;
 
+        this.graphqlConfiguration = context.causewayConfiguration.getViewer().getGraphql();
+
         if (isBuilt()) {
             // type already exists, nothing else to do.
             this.blobName = null;
@@ -56,7 +60,7 @@
 
         addChildFieldFor(blobName = new GqlvPropertyGetBlobBytes(this, context));
         addChildFieldFor(blobMimeType = new GqlvPropertyGetBlobMimeType(this, context));
-        addChildFieldFor(blobBytes = new GqlvPropertyGetBlobName(this, context));
+        addChildFieldFor(blobBytes = isResourceNotForbidden() ? new GqlvPropertyGetBlobName(this, context) : null);
 
         setField(newFieldDefinition()
                     .name("get")
@@ -64,6 +68,10 @@
                     .build());
     }
 
+    private boolean isResourceNotForbidden() {
+        return graphqlConfiguration.getResources().getResponseType() != CausewayConfiguration.Viewer.Graphql.ResponseType.FORBIDDEN;
+    }
+
     @Override
     protected Object fetchData(final DataFetchingEnvironment dataFetchingEnvironment) {
         return BookmarkedPojo.sourceFrom(dataFetchingEnvironment, context);
@@ -76,7 +84,9 @@
         }
         blobName.addDataFetcher(this);
         blobMimeType.addDataFetcher(this);
-        blobBytes.addDataFetcher(this);
+        if (blobBytes != null) {
+            blobBytes.addDataFetcher(this);
+        }
     }
 
     @Override
diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlobBytes.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlobBytes.java
index 2c1fe83..5c6fee5 100644
--- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlobBytes.java
+++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlobBytes.java
@@ -30,10 +30,14 @@
 
 public class GqlvPropertyGetBlobBytes extends GqlvPropertyGetBlobAbstract {
 
+    private final String graphqlPath;
+
     public GqlvPropertyGetBlobBytes(
             final Holder holder,
             final Context context) {
         super(holder, context, "bytes");
+
+        this.graphqlPath = context.causewayConfiguration.valueOf("spring.graphql.path").orElse("/graphql");
     }
 
     @Override
@@ -42,7 +46,7 @@
 
         Optional<Bookmark> bookmarkIfAny = context.bookmarkService.bookmarkFor(sourcePojo);
         return bookmarkIfAny.map(x -> String.format(
-                "///%s/object/%s:%s/%s/blobBytes", "graphql", x.getLogicalTypeName(), x.getIdentifier(), holder.getObjectAssociation().getId())).orElse(null);
+                "//%s/object/%s:%s/%s/blobBytes", graphqlPath, x.getLogicalTypeName(), x.getIdentifier(), holder.getObjectAssociation().getId())).orElse(null);
 
     }
 
diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClob.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClob.java
index 6983f85..75e756d 100644
--- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClob.java
+++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClob.java
@@ -22,6 +22,7 @@
 
 import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition;
 
+import org.apache.causeway.core.config.CausewayConfiguration;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
 import org.apache.causeway.viewer.graphql.model.context.Context;
@@ -35,9 +36,11 @@
 {
 
     final Holder holder;
-    final GqlvPropertyGetClobChars clobChars;
-    final GqlvPropertyGetClobMimeType clobMimeType;
     final GqlvPropertyGetClobName clobName;
+    final GqlvPropertyGetClobMimeType clobMimeType;
+    final GqlvPropertyGetClobChars clobChars;
+
+    private final CausewayConfiguration.Viewer.Graphql graphqlConfiguration;
 
     public GqlvPropertyGetClob(
             final Holder holder,
@@ -45,16 +48,19 @@
         super(TypeNames.propertyBlobTypeNameFor(holder.getObjectSpecification(), holder.getObjectMember()), context);
         this.holder = holder;
 
-        if(isBuilt()) {
-            this.clobChars = null;
-            this.clobMimeType = null;
+        this.graphqlConfiguration = context.causewayConfiguration.getViewer().getGraphql();
+
+        if (isBuilt()) {
+            // type already exists, nothing else to do.
             this.clobName = null;
+            this.clobMimeType = null;
+            this.clobChars = null;
             return;
         }
 
-        addChildFieldFor(clobChars = new GqlvPropertyGetClobChars(this, context));
-        addChildFieldFor(clobMimeType = new GqlvPropertyGetClobMimeType(this, context));
         addChildFieldFor(clobName = new GqlvPropertyGetClobName(this, context));
+        addChildFieldFor(clobMimeType = new GqlvPropertyGetClobMimeType(this, context));
+        addChildFieldFor(clobChars = isResourceNotForbidden() ? new GqlvPropertyGetClobChars(this, context) : null);
 
         setField(newFieldDefinition()
                     .name("get")
@@ -62,6 +68,10 @@
                     .build());
     }
 
+    private boolean isResourceNotForbidden() {
+        return graphqlConfiguration.getResources().getResponseType() != CausewayConfiguration.Viewer.Graphql.ResponseType.FORBIDDEN;
+    }
+
     @Override
     protected Object fetchData(final DataFetchingEnvironment dataFetchingEnvironment) {
         return BookmarkedPojo.sourceFrom(dataFetchingEnvironment, context);
@@ -69,12 +79,14 @@
 
     @Override
     protected void addDataFetchersForChildren() {
-        if(clobChars == null) {
+        if(clobName == null) {
             return;
         }
-        clobChars.addDataFetcher(this);
-        clobMimeType.addDataFetcher(this);
         clobName.addDataFetcher(this);
+        clobMimeType.addDataFetcher(this);
+        if(clobChars != null) {
+            clobChars.addDataFetcher(this);
+        }
     }
 
     @Override
diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClobChars.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClobChars.java
index fd59b02..6acff31 100644
--- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClobChars.java
+++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClobChars.java
@@ -30,10 +30,14 @@
 
 public class GqlvPropertyGetClobChars extends GqlvPropertyGetClobAbstract {
 
+    private final String graphqlPath;
+
     public GqlvPropertyGetClobChars(
             final Holder holder,
             final Context context) {
-        super(holder, context, "bytes");
+        super(holder, context, "chars");
+
+        this.graphqlPath = context.causewayConfiguration.valueOf("spring.graphql.path").orElse("/graphql");
     }
 
     @Override
@@ -42,7 +46,7 @@
 
         Optional<Bookmark> bookmarkIfAny = context.bookmarkService.bookmarkFor(sourcePojo);
         return bookmarkIfAny.map(x -> String.format(
-                "///%s/object/%s:%s/%s/clobChars", "graphql", x.getLogicalTypeName(), x.getIdentifier(), holder.getObjectAssociation().getId())).orElse(null);
+                "//%s/object/%s:%s/%s/clobChars", graphqlPath, x.getLogicalTypeName(), x.getIdentifier(), holder.getObjectAssociation().getId())).orElse(null);
 
     }
 
diff --git a/viewers/graphql/test/src/test/resources/application-test.properties b/viewers/graphql/test/src/test/resources/application-test.properties
index cbfd204..514fd18 100644
--- a/viewers/graphql/test/src/test/resources/application-test.properties
+++ b/viewers/graphql/test/src/test/resources/application-test.properties
@@ -26,4 +26,6 @@
 decorator.datasource.datasource-proxy.json-format=false
 
 # Enable Query Metrics
-decorator.datasource.datasource-proxy.count-query=false
\ No newline at end of file
+decorator.datasource.datasource-proxy.count-query=false
+
+causeway.viewer.graphql.resources.responseType=ATTACHMENT
\ No newline at end of file
diff --git a/viewers/graphql/test/src/test/resources/schema.gql b/viewers/graphql/test/src/test/resources/schema.gql
index 2e857f1..99035dc 100644
--- a/viewers/graphql/test/src/test/resources/schema.gql
+++ b/viewers/graphql/test/src/test/resources/schema.gql
@@ -218,8 +218,6 @@
 
 type causeway_applib_DomainObjectList__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -266,8 +264,6 @@
 
 type causeway_applib_FacetGroupNode__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -298,8 +294,6 @@
 
 type causeway_applib_ParameterNode__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -341,8 +335,6 @@
 
 type causeway_applib_PropertyNode__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -394,8 +386,6 @@
 
 type causeway_applib_RoleMemento__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -436,8 +426,6 @@
 
 type causeway_applib_TypeNode__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -498,8 +486,6 @@
 
 type causeway_applib_UserMemento__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -614,8 +600,6 @@
 
 type causeway_applib_node_ActionNode__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -666,8 +650,6 @@
 
 type causeway_applib_node_CollectionNode__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -717,8 +699,6 @@
 
 type causeway_applib_node_FacetAttrNode__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -760,8 +740,6 @@
 
 type causeway_applib_node_FacetNode__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -805,8 +783,6 @@
 
 type causeway_conf_ConfigurationProperty__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -848,8 +824,6 @@
 
 type causeway_conf_ConfigurationViewmodel__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -881,8 +855,6 @@
 
 type causeway_feat_ApplicationFeatureViewModel__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -942,8 +914,6 @@
 
 type causeway_feat_ApplicationNamespace__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1019,8 +989,6 @@
 
 type causeway_feat_ApplicationTypeAction__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1102,8 +1070,6 @@
 
 type causeway_feat_ApplicationTypeCollection__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1156,8 +1122,6 @@
 
 type causeway_feat_ApplicationTypeMember__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1223,8 +1187,6 @@
 
 type causeway_feat_ApplicationTypeProperty__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1310,8 +1272,6 @@
 
 type causeway_feat_ApplicationType__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1412,8 +1372,6 @@
 
 type causeway_schema_metamodel_v2_DomainClassDto__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1472,8 +1430,6 @@
 
 type causeway_security_LoginRedirect__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1520,8 +1476,6 @@
 
 type causeway_testing_fixtures_FixtureResult__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1552,8 +1506,6 @@
 
 type java_lang_Runnable__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1567,8 +1519,6 @@
 
 type java_util_Map__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1582,8 +1532,6 @@
 
 type java_util_SortedMap__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1597,8 +1545,6 @@
 
 type java_util_concurrent_Callable__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1612,8 +1558,6 @@
 
 type java_util_function_BiFunction__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1627,8 +1571,6 @@
 
 type java_util_function_Consumer__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1642,8 +1584,6 @@
 
 type java_util_function_Function__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1657,8 +1597,6 @@
 
 type java_util_stream_Stream__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1680,8 +1618,6 @@
 
 type org_apache_causeway_core_metamodel_inspect_model_MMNode__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1712,8 +1648,6 @@
 
 type org_apache_causeway_core_metamodel_inspect_model_MemberNode__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -1756,8 +1690,6 @@
 
 type org_apache_causeway_testing_fixtures_applib_fixturescripts_FixtureScript__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -2752,8 +2684,6 @@
 
 type university_dept_Department__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -2927,8 +2857,6 @@
 
 type university_dept_DeptHead__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -3007,8 +2935,6 @@
 
 type university_dept_StaffMember__gqlv_meta {
   cssClass: String
-  grid: String
-  icon: String
   id: String!
   layout: String
   logicalTypeName: String!
@@ -3048,7 +2974,6 @@
 type university_dept_StaffMember__photo__gqlv_property_blob {
   bytes: String
   mimeType: String
-  name: String
 }
 
 type university_dept_Staff__createStaffMember__department__gqlv_action_parameter {
diff --git a/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/controller/ResourceController.java b/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/controller/ResourceController.java
index 8e792e9..4ea4ff9 100644
--- a/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/controller/ResourceController.java
+++ b/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/controller/ResourceController.java
@@ -7,12 +7,14 @@
 
 import org.apache.causeway.applib.layout.grid.Grid;
 import org.apache.causeway.commons.io.JaxbUtils;
+import org.apache.causeway.core.config.CausewayConfiguration;
 import org.apache.causeway.core.metamodel.facets.object.grid.GridFacet;
 
 import org.apache.causeway.core.metamodel.facets.object.icon.ObjectIcon;
 
 import org.springframework.http.ContentDisposition;
 import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.lang.Nullable;
@@ -30,17 +32,26 @@
 import org.apache.causeway.core.metamodel.objectmanager.ObjectManager;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
 
-import lombok.RequiredArgsConstructor;
 import lombok.Value;
 import lombok.val;
 
 @RestController()
 @RequestMapping("/graphql/object")
-@RequiredArgsConstructor(onConstructor_ = {@Inject})
 public class ResourceController {
 
     private final BookmarkService bookmarkService;
     private final ObjectManager objectManager;
+    private final CausewayConfiguration.Viewer.Graphql graphqlConfiguration;
+
+    @Inject
+    public ResourceController(
+            final BookmarkService bookmarkService,
+            final ObjectManager objectManager,
+            final CausewayConfiguration causewayConfiguration) {
+        this.bookmarkService = bookmarkService;
+        this.objectManager = objectManager;
+        this.graphqlConfiguration = causewayConfiguration.getViewer().getGraphql();
+    }
 
     @GetMapping(value = "/{logicalTypeName}:{id}/{propertyId}/blobBytes")
     public ResponseEntity<byte[]> propertyBlobBytes(
@@ -48,14 +59,26 @@
             @PathVariable final String id,
             @PathVariable final String propertyId
     ) {
+        val responseType = graphqlConfiguration.getResources().getResponseType();
+
+        // TODO: perhaps a filter would factor this check out?
+        if (responseType == CausewayConfiguration.Viewer.Graphql.ResponseType.FORBIDDEN) {
+            return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
+        }
+
         return valueOfProperty(logicalTypeName, id, propertyId)
                 .filter(Blob.class::isInstance)
                 .map(Blob.class::cast)
-                .map(blob -> ResponseEntity.ok()
-                        .contentType(MediaType.asMediaType(MimeType.valueOf(blob.getMimeType().toString())))
-                        .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(blob.getName()).build().toString())
-                        .contentLength(blob.getBytes().length)
-                        .body(blob.getBytes()))
+                .map(blob -> {
+                    val bodyBuilder = ResponseEntity.ok()
+                            .contentType(MediaType.asMediaType(MimeType.valueOf(blob.getMimeType().toString())));
+                    if (responseType == CausewayConfiguration.Viewer.Graphql.ResponseType.ATTACHMENT) {
+                        bodyBuilder
+                                .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(blob.getName()).build().toString())
+                                .contentLength(blob.getBytes().length);
+                    }
+                    return bodyBuilder.body(blob.getBytes());
+                })
                 .orElse(ResponseEntity.notFound().build());
     }
 
@@ -65,50 +88,91 @@
             @PathVariable final String id,
             @PathVariable final String propertyId
     ) {
+        val responseType = graphqlConfiguration.getResources().getResponseType();
+
+        // TODO: perhaps a filter would factor this check out?
+        if (responseType == CausewayConfiguration.Viewer.Graphql.ResponseType.FORBIDDEN) {
+            return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
+        }
+
         return valueOfProperty(logicalTypeName, id, propertyId)
                 .filter(Clob.class::isInstance)
                 .map(Clob.class::cast)
-                .map(clob -> ResponseEntity.ok()
-                        .contentType(MediaType.asMediaType(MimeType.valueOf(clob.getMimeType().toString())))
-                        .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(clob.getName()).build().toString())
-                        .contentLength(clob.getChars().length())
-                        .body(clob.getChars()))
+                .map(clob -> {
+                    val bodyBuilder = ResponseEntity.ok()
+                            .contentType(MediaType.asMediaType(MimeType.valueOf(clob.getMimeType().toString())));
+                    if (responseType == CausewayConfiguration.Viewer.Graphql.ResponseType.ATTACHMENT) {
+                        bodyBuilder.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(clob.getName()).build().toString())
+                                .contentLength(clob.getChars().length());
+                    }
+                    return bodyBuilder.body(clob.getChars());
+                })
                 .orElse(ResponseEntity.notFound().build());
     }
 
-    @GetMapping(value = "/{logicalTypeName}:{id}/_meta/grid")
+    @GetMapping(value = "/{logicalTypeName}:{id}/{_meta}/grid")
     public ResponseEntity<String> grid(
             @PathVariable final String logicalTypeName,
-            @PathVariable final String id
+            @PathVariable final String id,
+            @PathVariable final String _meta
     ) {
+        val responseType = graphqlConfiguration.getResources().getResponseType();
+
+        // TODO: perhaps a filter would factor this check out?
+        if (responseType == CausewayConfiguration.Viewer.Graphql.ResponseType.FORBIDDEN) {
+            return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
+        }
+
+        if (!_meta.equals(graphqlConfiguration.getMetaData().getFieldName())) {
+            return ResponseEntity.notFound().build();
+        }
+
         return lookup(logicalTypeName, id)
                 .map(ResourceController::gridOf)
                 .filter(Objects::nonNull)
                 .map(JaxbUtils::toStringUtf8)
                 .map(x -> x.replaceAll("(\r\n)", "\n"))
-                .map(gridText -> ResponseEntity.ok()
-                        .contentType(MediaType.APPLICATION_XML)
-                        .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(logicalTypeName + ".layout.xml").build().toString())
-                        .contentLength(gridText.length())
-                        .body(gridText))
+                .map(gridText -> {
+                    val bodyBuilder = ResponseEntity.ok()
+                            .contentType(MediaType.APPLICATION_XML);
+                    if (responseType == CausewayConfiguration.Viewer.Graphql.ResponseType.ATTACHMENT) {
+                        bodyBuilder
+                                .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(logicalTypeName + ".layout.xml").build().toString())
+                                .contentLength(gridText.length());
+                    }
+                    return bodyBuilder.body(gridText);
+                })
                 .orElse(ResponseEntity.notFound().build());
     }
 
-    @GetMapping(value = "/{logicalTypeName}:{id}/_meta/icon")
+    @GetMapping(value = "/{logicalTypeName}:{id}/{_meta}/icon")
     public ResponseEntity<byte[]> icon(
             @PathVariable final String logicalTypeName,
-            @PathVariable final String id
+            @PathVariable final String id,
+            @PathVariable final String _meta
     ) {
+        // TODO: perhaps a filter would factor this check out?
+        val responseType = graphqlConfiguration.getResources().getResponseType();
+        if (responseType == CausewayConfiguration.Viewer.Graphql.ResponseType.FORBIDDEN) {
+            return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
+        }
+        if (!_meta.equals(graphqlConfiguration.getMetaData().getFieldName())) {
+            return ResponseEntity.notFound().build();
+        }
+
         return lookup(logicalTypeName, id)
                 .map(ManagedObject::getIcon)
                 .filter(Objects::nonNull)
                 .map(objectIcon -> {
                     val bytes = objectIcon.asBytes();
-                    return ResponseEntity.ok()
-                            .contentType(MediaType.parseMediaType(objectIcon.getMimeType().getMimeType().toString()))
-                            .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(logicalTypeName + ".png").build().toString())
-                            .contentLength(bytes.length)
-                            .body(bytes);
+                    val bodyBuilder = ResponseEntity.ok()
+                            .contentType(MediaType.parseMediaType(objectIcon.getMimeType().getMimeType().toString()));
+                    if (responseType == CausewayConfiguration.Viewer.Graphql.ResponseType.ATTACHMENT) {
+                        bodyBuilder
+                                .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(logicalTypeName + ".png").build().toString())
+                                .contentLength(bytes.length);
+                    }
+                    return bodyBuilder.body(bytes);
                 })
                 .orElse(ResponseEntity.notFound().build());
     }
@@ -119,11 +183,6 @@
         return facet != null ? facet.getGrid(managedObject) : null;
     }
 
-    @Nullable
-    private static ObjectIcon iconOf(ManagedObject managedObject) {
-        return managedObject.getIcon();
-    }
-
     private Optional<Object> valueOfProperty(String logicalTypeName, String id, String propertyId) {
         return lookup(logicalTypeName, id)
                 .map(managedObject -> ManagedObjectAndPropertyIfAny.of(managedObject, managedObject.getSpecification().getProperty(propertyId)))