Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a way to configure own SerializerAdapter instance while building XxxBlob clients #24374

Closed
wants to merge 1 commit into from

Conversation

reta
Copy link
Contributor

@reta reta commented Sep 27, 2021

At the moment, XxxBlob client builders (BlobClientBuilder, BlobContainerClientBuilder, BlobServiceClientBuilder, ...) do not provide a way to supply own SerializerAdapter instance and rely on JacksonAdapter::createDefaultSerializerAdapter. It works in most cases but sometimes, fe. when application runs under SecurityManager and needs to use reflection, it is not sufficient and using custom SerializerAdapter would be very helpful. The underlying AzureBlobStorageImpl already supports that however the ability to pass the SerializerAdapter instance down the chain of builders is not supported.

The PR provides a way for the XxxBlob client builders to allow using user-provided SerializerAdapter instance. The change is low risk (arguably) and does not break an existing APIs.

Fixes #24373.
Thank you!

@ghost ghost added Storage Storage Service (Queues, Blobs, Files) customer-reported Issues that are reported by GitHub users external to the Azure organization. labels Sep 27, 2021
@ghost
Copy link

ghost commented Sep 27, 2021

Thank you for your contribution reta! We will review the pull request and get back to you soon.

@alzimmermsft
Copy link
Member

Thanks for submitting the issues and filing this PR @reta!

Just have a few questions around specifics you were seeing which resulted in SecurityManager throwing errors in an application.

  • Did the reflective access failure occur when JacksonAdapter attempted to reflectively access a model type in a com.azure package?

    • The way the SDKs are designed is that com.azure models needs to be accessible by Jackson and com.azure.core, so it may be the case where one of these isn't holding true.
  • What is your expected use case when being able to configure the SerializerAdapter? Is it going to be a specific Thread with additional permissions?

  • What APIs were being used when the exception was seen? Was BinaryData being used?

@reta
Copy link
Contributor Author

reta commented Sep 28, 2021

Thanks a lot for looking, @alzimmermsft ! It would be my pleasure to share more details.

Did the reflective access failure occur when JacksonAdapter attempted to reflectively access a model type in a com.azure package?

This is correct, the kind of exceptions we are seeing:

Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot access public com.azure.storage.blob.implementation.models.ListBlobsHierarchySegmentResponse() (from class com.azure.storage.blob.implementation.models.ListBlobsHierarchySegmentResponse; failed to set access: access denied ("java.lang.reflect.ReflectPermission" "suppressAccessChecks")
 at [Source: (byte[])"<?xml version="1.0" encoding="UTF-8"?><EnumerationResults><Delimiter>/</Delimiter><Blobs></Blobs></EnumerationResults>"; line: 1, column: 59]
	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:62)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:268)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:150)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:414)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:349)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:264)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)
	at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142)
	at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:591)
	at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:4733)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4594)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3643)
	at com.azure.core.util.serializer.JacksonAdapter.deserialize(JacksonAdapter.java:284)
        ....
Caused by: java.lang.IllegalArgumentException: Cannot access public com.azure.storage.blob.implementation.models.ListBlobsHierarchySegmentResponse() (from class com.azure.storage.blob.implementation.models.ListBlobsHierarchySegmentResponse; failed to set access: access denied ("java.lang.reflect.ReflectPermission" "suppressAccessChecks")
	at com.fasterxml.jackson.databind.util.ClassUtil.checkAndFixAccess(ClassUtil.java:994)
	at com.fasterxml.jackson.databind.deser.impl.CreatorCollector._fixAccess(CreatorCollector.java:278)
	at com.fasterxml.jackson.databind.deser.impl.CreatorCollector.setDefaultCreator(CreatorCollector.java:130)
	at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._addExplicitConstructorCreators(BasicDeserializerFactory.java:446)
	at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._constructDefaultValueInstantiator(BasicDeserializerFactory.java:304)
	at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.findValueInstantiator(BasicDeserializerFactory.java:223)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:2

(I cut stacktraces to the relevant parts, please let me know if you need full ones, I think the picture is more or less clear).

The way the SDKs are designed is that com.azure models needs to be accessible by Jackson and com.azure.core, so it may be the case where one of these isn't holding true.

That would be my expectations as well, it seems like certain kind of model classes do trigger accessibility "correction" path in Jackson Databind, I could say with the high confidence that it is not a single model class but many.

What is your expected use case when being able to configure the SerializerAdapter? Is it going to be a specific Thread with additional permissions?

Yes, the thread pools are aware of the SecurityManager and are using the relevant thread group. But the need for priviledge access checks are missed in Jackson / ClassUtil (I could probably come up with the fix there), so in case of the SerializerAdapter, the serialization / deserialization methods have to be wrapped into PrivilegedAction.

What APIs were being used when the exception was seen? Was BinaryData being used?

A number of the APIs to be fair, upload probably pops up more often than others (primarily chunked, commit / stage / ...), also list by prefix / hierarchy.

Thank you for asking these questions. Although we run into some specific limitations, I think the ability to supply own serialization / deserialization laeyr or / and instrumentation comes up all the time, and Azure SDK has full support for that under the hood.

Thank you!

@reta
Copy link
Contributor Author

reta commented Sep 29, 2021

@alzimmermsft went down the Jackson Databind hole, additional context FasterXML/jackson-databind#997 and FasterXML/jackson-databind#992. Probably, another argument to allow SerializerAdapter configuration/customization :-) (fe, to enable/disable MappingFeatures, ...). Thank you.

@reta
Copy link
Contributor Author

reta commented Oct 7, 2021

@alzimmermsft @alzimmermsft apologies, any concerns left unaddressed? Thank you!

@alzimmermsft
Copy link
Member

@alzimmermsft @alzimmermsft apologies, any concerns left unaddressed? Thank you!

I'd actually want to take a crack at this by changing the default serialization configurations in JacksonAdapter to play better with SecurityManger before exposing additional APIs. There's a lot of configuration in JacksonAdapter that is explicitly done to work with the model types used in the SDKs, which makes it brittle and could break runtime behavior easily.

Would you be willing to share your SecurityManager configuration being used that results in these exceptions being thrown. Or, at least, a simple configurations that results in a replication of the exceptions.

@reta
Copy link
Contributor Author

reta commented Oct 12, 2021

Thanks @alzimmermsft

There's a lot of configuration in JacksonAdapter that is explicitly done to work with the model types used in the SDKs, which makes it brittle and could break runtime behavior easily.

Valid concerns, it has at least 3 different serializers, only one is somewhat configurable, not others.

Would you be willing to share your SecurityManager configuration being used that results in these exceptions being thrown. Or, at least, a simple configurations that results in a replication of the exceptions.

Will try to provide minimal example shortly

@reta
Copy link
Contributor Author

reta commented Oct 13, 2021

@alzimmermsft here we are
azure-security-adapter.zip

So what this example does:

  • there are two modules: io.aven.security.client and io.aven.security.server
  • io.aven.security.server sets the SecurityManager and calls APIs from io.aven.security.client
  • io.aven.security.client uses JacksonAdapter to deserialize some sample Azure SDK payload
  • the security policy test-security.policy grants all necessary permissions to io.aven.security.client (specifically, java.lang.reflect.ReflectPermission "suppressAccessChecks") but not to io.aven.security.server, it is not supposed to have them

To run the example: mvn test

The output (below) demonstrates the regular (unpriviledged) and priviledged calls. The issue we are dealing with is that ClassUtil.checkAndFixAccess fails, because the caller io.aven.security.server does not have java.lang.reflect.ReflectPermission "suppressAccessChecks". The solution in this particular case is to use AccessController.doPrivileged in the io.aven.security.client (it is trusted) and the deserialization succeeds (for the reasons that AccessController.doPrivileged establishes new chain of trust without io.aven.security.server).

Unpriviledged Access: 
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot access public com.azure.storage.blob.implementation.models.ListBlobsHierarchySegmentResponse() (from class com.azure.storage.blob.implementation.models.ListBlobsHierarchySegmentResponse; failed to set access: access denied ("java.lang.reflect.ReflectPermission" "suppressAccessChecks")
 at [Source: (BufferedInputStream); line: 2, column: 1]
	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:62)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:268)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:150)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:414)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:349)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:264)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)
	at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142)
	at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:591)
	at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:4733)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4594)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3601)
	at com.azure.core.util.serializer.JacksonAdapter.deserialize(JacksonAdapter.java:296)
	at io.aven.security.client.AzureRepository.snapshot(AzureRepository.java:22)
	at io.aven.security.server.AzureSecurityRunner.main(AzureSecurityRunner.java:28)
	at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:254)
	at java.base/java.lang.Thread.run(Thread.java:829)
Caused by: java.lang.IllegalArgumentException: Cannot access public com.azure.storage.blob.implementation.models.ListBlobsHierarchySegmentResponse() (from class com.azure.storage.blob.implementation.models.ListBlobsHierarchySegmentResponse; failed to set access: access denied ("java.lang.reflect.ReflectPermission" "suppressAccessChecks")
	at com.fasterxml.jackson.databind.util.ClassUtil.checkAndFixAccess(ClassUtil.java:994)
	at com.fasterxml.jackson.databind.deser.impl.CreatorCollector._fixAccess(CreatorCollector.java:278)
	at com.fasterxml.jackson.databind.deser.impl.CreatorCollector.setDefaultCreator(CreatorCollector.java:130)
	at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._addExplicitConstructorCreators(BasicDeserializerFactory.java:446)
	at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._constructDefaultValueInstantiator(BasicDeserializerFactory.java:304)
	at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.findValueInstantiator(BasicDeserializerFactory.java:223)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:261)
	... 15 more

Priviledged Access: 
com.azure.storage.blob.implementation.models.ListBlobsHierarchySegmentResponse@214306fe

This is basically it. If you have any further questions, please let me know. Alternative solution for this particular issues would be the ability to configure MapperFeatures (FasterXML/jackson-databind#997), it is cumbersome in JacksonAdapter since it has at least 3 different mappers, and only one is somewhat exposed and partially configurable.

@alzimmermsft
Copy link
Member

Thank you so much for the reproduction @reta. I've been testing out what would happen if the mentioned configuration is disabled and it appears to trigger a new issue where Jackson isn't able to access a private field on a class.

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Cannot access java.lang.String com.azure.storage.blob.implementation.models.ListBlobsHierarchySegmentResponse.serviceEndpoint (from class com.azure.storage.blob.implementation.models.ListBlobsHierarchySegmentResponse; failed to set access: access denied ("java.lang.reflect.ReflectPermission" "suppressAccessChecks")
 at [Source: (BufferedInputStream); line: 2, column: 1]

Also, appears there is a second configuration which allows for setAccessible to be called, MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS.

I'll continue looking at other ways this issue can be remediated.

@alzimmermsft
Copy link
Member

alzimmermsft commented Oct 19, 2021

@reta, this is still being looked into, and unfortunately this has opened a bigger "can of worms" than initially expected. As mentioned previously, preventing Jackson from setAccessible(true) on constructors, fields, or methods results in deserialization failing to access inaccessible constructors, fields, or methods during runtime, instead of the SecurityManager exceptions of attempting to setAccessible. As you've likely seen, this is due to how our model types are annotated and handled in serialization scenarios by relying on private field access for mutating the deserialization object or for accessing the field value during serialization.

Looking forward, we're looking into ways to begin remedying this through a few different avenues:

  • Researching usage of MethodHandles.Lookup passed from SDKs into serialization to offer an "on behalf of" style functionality. This should get around the need to setAccessible(true) as the MethodHandles.Lookup would have private lookup capabilities for a given SDK as it created the lookup.
  • Changing the pattern of how POJOs are annotated and handled in serialization to limit the number of scenarios where non-public constructors, fields, or methods need to be used.

Unfortunately, both of these resolutions are much, much more comprehensive and will likely take a few months to implement and verify that there are no regressions. Given that, I would love to explore other potential avenues we could take in the meantime to unblock the issue you're running into. In the replication for this issue you gave you had a code path which leveraged AccessController.doPrivileged, would this be something that could unblock your application for now?

cc: @JonathanGiles @lmolkova @srnagar

@reta
Copy link
Contributor Author

reta commented Oct 19, 2021

Thanks a lot for the detailed investigation @alzimmermsft , I certainly understand the complexity involved.

In the replication for this issue you gave you had a code path which leveraged AccessController.doPrivileged, would this be something that could unblock your application for now?

That is right, the cleanest way to do that is to use the AccessController.doPriviledged during the serialization / deserialization, preventing the need to weaken broader security constraints. It could be done either using inheritance or delegation, just a quick example:

public class PriviledgedJacksonAdapter extends JacksonAdapter {
   @Override
   public String serialize(Object object, SerializerEncoding encoding) throws IOException {
       return AccessController.doPriviledged(() -> super.serialize(object, encoding));
   }
}

But the showstopper is this case is the inability to provide own SerializerAdapter, hence the feature request and this pull request, so to have a path to decorate serialization / deserialization with AccessController.doPriviledged blocks. The last resort is to run a whole HTTP worker thread (Netty or OkHttp) within AccessController.doPriviledged block :-(

Thank you @alzimmermsft !

@alzimmermsft
Copy link
Member

Apologies on the slower reply @reta.

I've filed this draft PR which begins looking into offering an way for an application to pass what I call an "access helper". The access helpers purpose is a method that can help JacksonAdapter escape any global access restrictions by running serialization calls through an application provided, well-known and secure method which allows it to run serialization properly.

@reta
Copy link
Contributor Author

reta commented Oct 22, 2021

Thanks a lot, @alzimmermsft , really appreciate you guys looking deeply into the issue. The draft suggests another viable option, no objections from my side, please feel free to close this PR in favor of access helper. Thank you!

@alzimmermsft
Copy link
Member

Hi @reta, in the last release of azure-core (1.22.0) a new environment configuration (AZURE_JACKSON_ADAPTER_USE_ACCESS_HELPER) was added which escalates running JacksonAdapter serialization with AccessController: #25004

@reta
Copy link
Contributor Author

reta commented Nov 8, 2021

Thank you so much @alzimmermsft ! I am closing this one :)

@reta reta closed this Nov 8, 2021
azure-sdk pushed a commit to azure-sdk/azure-sdk-for-java that referenced this pull request Jun 8, 2023
Use conditional compile (Azure#24374)

* Add conditional compile of client.tsp

* Always execute from main.tsp, always check client.tsp

* Fix script

* Remove yaml reference

* Update specification/cognitiveservices/HealthInsights/healthinsights.oncophenotype/tspconfig.yaml

---------

Co-authored-by: Mike Harder <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
customer-reported Issues that are reported by GitHub users external to the Azure organization. Storage Storage Service (Queues, Blobs, Files)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[FEATURE REQ] Provide a way to configure own SerializerAdapter instance while building XxxBlob clients
2 participants