This is an automated email from the ASF dual-hosted git repository. joerghoh pushed a commit to branch SLING-11924-prevent-rr-serialization in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-models-jacksonexporter.git
commit e47008dd1af6fcfcfd47648e6d0e7365092b30d7 Author: Joerg Hoh <[email protected]> AuthorDate: Sun Jul 2 15:06:19 2023 +0200 SLING-11924 disallow the serialization of a ResourceResolver --- pom.xml | 10 +- .../impl/IgnoringResourceResolverMixin.java | 65 +++++ .../impl/ResourceResolverModuleProvider.java | 63 +++++ .../impl/WarningResourceResolverMixin.java | 73 +++++ .../JacksonExporterLimitSerializationTest.java | 311 +++++++++++++++++++++ .../impl/example/PojoWithResourceResolver.java | 36 +++ .../jacksonexporter/impl/util/LogCapture.java | 69 +++++ 7 files changed, 625 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 64872f9..1285634 100644 --- a/pom.xml +++ b/pom.xml @@ -152,10 +152,16 @@ </dependency> <dependency> <groupId>org.apache.sling</groupId> - <artifactId>org.apache.sling.testing.logging-mock</artifactId> - <version>2.0.0</version> + <artifactId>org.apache.sling.testing.sling-mock.junit5</artifactId> + <version>3.2.0</version> <scope>test</scope> </dependency> + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-classic</artifactId> + <version>1.2.3</version> + <scope>test</scope> + </dependency> </dependencies> diff --git a/src/main/java/org/apache/sling/models/jacksonexporter/impl/IgnoringResourceResolverMixin.java b/src/main/java/org/apache/sling/models/jacksonexporter/impl/IgnoringResourceResolverMixin.java new file mode 100644 index 0000000..c7f56e4 --- /dev/null +++ b/src/main/java/org/apache/sling/models/jacksonexporter/impl/IgnoringResourceResolverMixin.java @@ -0,0 +1,65 @@ +/* + * 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.sling.models.jacksonexporter.impl; + +import java.io.IOException; + +import org.apache.sling.api.resource.ResourceResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonIgnoreType; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +/** + * This mixin disables the serialization of the ResourceResolver; if it's still requested, a ERROR log message is written. + * + */ + +@JsonIgnoreType +public abstract interface IgnoringResourceResolverMixin extends ResourceResolver { + + public static final String MESSAGE = "The serialization of a ResourceResolver was rejected, because the JacksonExporter servlet " + + "is configured to do so. Please review your Sling Model implementation class(es) and remove any public reference to a " + + "ResourceResolver."; + + public static final Logger LOG = LoggerFactory.getLogger(JacksonExporter.class); + + + @JsonSerialize(using=JustLoggingSerializer.class) + @Override + boolean isLive(); + + + + public class JustLoggingSerializer extends JsonSerializer<Boolean> { + + @Override + public void serialize(Boolean value, JsonGenerator jgen, SerializerProvider provider) + throws IOException, JsonProcessingException { + LOG.warn(MESSAGE); + } + + } + +} diff --git a/src/main/java/org/apache/sling/models/jacksonexporter/impl/ResourceResolverModuleProvider.java b/src/main/java/org/apache/sling/models/jacksonexporter/impl/ResourceResolverModuleProvider.java new file mode 100644 index 0000000..63dd4d6 --- /dev/null +++ b/src/main/java/org/apache/sling/models/jacksonexporter/impl/ResourceResolverModuleProvider.java @@ -0,0 +1,63 @@ +/* + * 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.sling.models.jacksonexporter.impl; + +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.models.jacksonexporter.ModuleProvider; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.module.SimpleModule; + +@Component(service = ModuleProvider.class) +@Designate(ocd = ResourceResolverModuleProvider.Config.class) +public class ResourceResolverModuleProvider implements ModuleProvider { + + @ObjectClassDefinition(name = "Apache Sling Models Jackson Exporter - ResourceResolver support", + description = "Provider of a Jackson Module which enables/disables support for serializing a ResourceResolver") + static @interface Config { + + @AttributeDefinition(name ="disable serialization of the ResourceResolver", + description = "if enabled, ResourceResolver instances are not longer deserialized as JSON") + boolean disable_serialization() default false; + + } + + SimpleModule moduleInstance; + + + @Activate + private void activate(Config config) { + this.moduleInstance = new SimpleModule(); + if (config.disable_serialization()) { + moduleInstance.setMixInAnnotation(ResourceResolver.class, IgnoringResourceResolverMixin.class); + } else { + moduleInstance.setMixInAnnotation(ResourceResolver.class, WarningResourceResolverMixin.class); + } + } + + + @Override + public Module getModule() { + return moduleInstance; + } + +} diff --git a/src/main/java/org/apache/sling/models/jacksonexporter/impl/WarningResourceResolverMixin.java b/src/main/java/org/apache/sling/models/jacksonexporter/impl/WarningResourceResolverMixin.java new file mode 100644 index 0000000..3a6cc2c --- /dev/null +++ b/src/main/java/org/apache/sling/models/jacksonexporter/impl/WarningResourceResolverMixin.java @@ -0,0 +1,73 @@ +/* + * 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.sling.models.jacksonexporter.impl; + +import java.io.IOException; + +import org.apache.sling.api.resource.ResourceResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + + +/** + * This mixin exports data which Jackson would export by default (so not change in the default behaviour), + * but prints a warning whenever it does that. + */ + + +@JsonAutoDetect +public interface WarningResourceResolverMixin extends ResourceResolver { + + public static final String MESSAGE = "A ResourceResolver is serialized with all its private fields containing " + + "implementation details you should not disclose. Please review your Sling Model implementation(s) and remove " + + "all public accessors to a ResourceResolver."; + + public static final Logger LOG = LoggerFactory.getLogger(JacksonExporter.class); + + + + // This method is explicitly mentioned so we provide a custom serializer which prints the warning + + @Override + @JsonSerialize(using=WarningBooleanSerializer.class) + boolean isLive(); + + + + public class WarningBooleanSerializer extends JsonSerializer<Boolean> { + + @Override + public void serialize(Boolean value, JsonGenerator jgen, SerializerProvider provider) + throws IOException, JsonProcessingException { + LOG.warn(MESSAGE); + jgen.writeObject(value); + + } + + } + + +} diff --git a/src/test/java/org/apache/sling/models/jacksonexporter/impl/JacksonExporterLimitSerializationTest.java b/src/test/java/org/apache/sling/models/jacksonexporter/impl/JacksonExporterLimitSerializationTest.java new file mode 100644 index 0000000..fff0582 --- /dev/null +++ b/src/test/java/org/apache/sling/models/jacksonexporter/impl/JacksonExporterLimitSerializationTest.java @@ -0,0 +1,311 @@ +/* + * 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.sling.models.jacksonexporter.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.sling.api.resource.LoginException; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.models.factory.ExportException; +import org.apache.sling.models.jacksonexporter.impl.example.PojoWithResourceResolver; +import org.apache.sling.models.jacksonexporter.impl.util.LogCapture; +import org.apache.sling.testing.mock.osgi.junit5.OsgiContext; +import org.apache.sling.testing.mock.osgi.junit5.OsgiContextExtension; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import ch.qos.logback.classic.Level; + +@ExtendWith(OsgiContextExtension.class) +public class JacksonExporterLimitSerializationTest { + + private OsgiContext context = new OsgiContext(); + + + + @Test + public void testWarnLogWhenSerializingResourceResolver() throws ExportException { + + LogCapture capture = new LogCapture(JacksonExporter.class.getName(),false); + + PojoWithResourceResolver pojo = new PojoWithResourceResolver("text", new EmptyResourceResolver()); + + context.registerInjectActivateService(new ResourceResolverModuleProvider()); + JacksonExporter underTest = context.registerInjectActivateService(JacksonExporter.class); + Map<String,String> options = Collections.emptyMap(); + + String expectedJson = "{\"msg\":\"text\",\"resolver\":{"; + assertTrue(underTest.export(pojo, String.class, options).contains(expectedJson)); + assertTrue(capture.anyMatch(event -> { + return event.getFormattedMessage().equals(WarningResourceResolverMixin.MESSAGE) && + event.getLevel().equals(Level.WARN); + })); + } + + + @Test + public void testNotSerializingResourceResolverWhenDisabled() throws ExportException { + + LogCapture capture = new LogCapture(IgnoringResourceResolverMixin.class.getName(),false); + PojoWithResourceResolver pojo = new PojoWithResourceResolver("text",new EmptyResourceResolver()); + + Map<String,Object> config = Collections.singletonMap("disable.serialization", "true"); + context.registerInjectActivateService(new ResourceResolverModuleProvider(),config); + + + JacksonExporter underTest = context.registerInjectActivateService(JacksonExporter.class); + Map<String,String> options = Collections.emptyMap(); + + String expectedJson = "{\"msg\":\"text\"}"; + assertEquals(expectedJson, underTest.export(pojo, String.class, options)); +// assertTrue(capture.anyMatch(p -> p.getFormattedMessage().contains(IgnoringResourceResolverMixin.MESSAGE))); + } + + + /** + * A very simple ResourceResolver implementation which does not lead to any issues with any mocking framework + * when trying to export it with Jackson. + */ + public class EmptyResourceResolver implements ResourceResolver { + +// public static final String SERIALIZED_STRING = "\"resolver\":{\"live\":false,\"userID\":null,\"searchPath\":null,\"propertyMap\":null,\"userID\":false}"; + + + @Override + public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Resource resolve(HttpServletRequest request, String absPath) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Resource resolve(String absPath) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Resource resolve(HttpServletRequest request) { + // TODO Auto-generated method stub + return null; + } + + @Override + public String map(String resourcePath) { + // TODO Auto-generated method stub + return null; + } + + @Override + public String map(HttpServletRequest request, String resourcePath) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Resource getResource(String path) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Resource getResource(Resource base, String path) { + // TODO Auto-generated method stub + return null; + } + + @Override + public String[] getSearchPath() { + // TODO Auto-generated method stub + return null; + } + + @Override + public Iterator<Resource> listChildren(Resource parent) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Resource getParent(Resource child) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Iterable<Resource> getChildren(Resource parent) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Iterator<Resource> findResources(String query, String language) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Iterator<Map<String, Object>> queryResources(String query, String language) { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean hasChildren(Resource resource) { + // TODO Auto-generated method stub + return false; + } + + @Override + public ResourceResolver clone(Map<String, Object> authenticationInfo) throws LoginException { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean isLive() { + // TODO Auto-generated method stub + return false; + } + + @Override + public void close() { + // TODO Auto-generated method stub + + } + + @Override + public String getUserID() { + // TODO Auto-generated method stub + return null; + } + + @Override + public Iterator<String> getAttributeNames() { + // TODO Auto-generated method stub + return null; + } + + @Override + public Object getAttribute(String name) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void delete(Resource resource) throws PersistenceException { + // TODO Auto-generated method stub + + } + + @Override + public Resource create(Resource parent, String name, Map<String, Object> properties) + throws PersistenceException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void revert() { + // TODO Auto-generated method stub + + } + + @Override + public void commit() throws PersistenceException { + // TODO Auto-generated method stub + + } + + @Override + public boolean hasChanges() { + // TODO Auto-generated method stub + return false; + } + + @Override + public String getParentResourceType(Resource resource) { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getParentResourceType(String resourceType) { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean isResourceType(Resource resource, String resourceType) { + // TODO Auto-generated method stub + return false; + } + + @Override + public void refresh() { + // TODO Auto-generated method stub + + } + + @Override + public Resource copy(String srcAbsPath, String destAbsPath) throws PersistenceException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Resource move(String srcAbsPath, String destAbsPath) throws PersistenceException { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean orderBefore(@NotNull Resource parent, @NotNull String name, + @Nullable String followingSiblingName) + throws UnsupportedOperationException, PersistenceException, IllegalArgumentException { + // TODO Auto-generated method stub + return false; + } + + @Override + public @NotNull Map<String, Object> getPropertyMap() { + // TODO Auto-generated method stub + return null; + } + + } + + +} diff --git a/src/test/java/org/apache/sling/models/jacksonexporter/impl/example/PojoWithResourceResolver.java b/src/test/java/org/apache/sling/models/jacksonexporter/impl/example/PojoWithResourceResolver.java new file mode 100644 index 0000000..3e544b7 --- /dev/null +++ b/src/test/java/org/apache/sling/models/jacksonexporter/impl/example/PojoWithResourceResolver.java @@ -0,0 +1,36 @@ +/* + * 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.sling.models.jacksonexporter.impl.example; + +import org.apache.sling.api.resource.ResourceResolver; + +public class PojoWithResourceResolver { + + + public String msg; + public ResourceResolver resolver; + + public PojoWithResourceResolver (String msg, ResourceResolver resolver) { + this.msg = msg; + this.resolver = resolver; + } + + + +} diff --git a/src/test/java/org/apache/sling/models/jacksonexporter/impl/util/LogCapture.java b/src/test/java/org/apache/sling/models/jacksonexporter/impl/util/LogCapture.java new file mode 100644 index 0000000..be328b0 --- /dev/null +++ b/src/test/java/org/apache/sling/models/jacksonexporter/impl/util/LogCapture.java @@ -0,0 +1,69 @@ +/* + * 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.sling.models.jacksonexporter.impl.util; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.Closeable; +import java.io.IOException; +import java.util.function.Predicate; + +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; + +public class LogCapture extends ListAppender<ILoggingEvent> implements Closeable { + private final boolean verboseFailure; + + /** Setup the capture and start it */ + public LogCapture(String loggerName, boolean verboseFailure) { + this.verboseFailure = verboseFailure; + Logger logger = (Logger) LoggerFactory.getLogger(loggerName); + logger.setLevel(Level.ALL); + setContext((LoggerContext) LoggerFactory.getILoggerFactory()); + logger.addAppender(this); + start(); + } + + public boolean anyMatch(Predicate<ILoggingEvent> p) { + return this.list.stream().anyMatch(p); + } + + public void assertMessage(String message) { + assertTrue(anyMatch(event -> { + return event.getFormattedMessage().equals(message); + })); + } + + + + + + @Override + public void close() throws IOException { + stop(); + } + + + +}
