Skip to content

Commit

Permalink
Improve serialize of proxy objects generated by AuthorizeReturnObject
Browse files Browse the repository at this point in the history
  • Loading branch information
kse-music committed Aug 28, 2024
1 parent 84fc5a7 commit a8e073e
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ public Object proxy(Object target) {
factory.addAdvisors(advisor);
}
factory.setProxyTargetClass(!Modifier.isFinal(target.getClass().getModifiers()));
factory.addInterface(AuthorizationProxy.class);
return factory.getProxy();
}

Expand Down Expand Up @@ -357,6 +358,7 @@ public Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object object
ProxyFactory factory = new ProxyFactory();
factory.setTargetClass(targetClass);
factory.setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass));
factory.addInterface(AuthorizationProxy.class);
factory.setProxyTargetClass(!Modifier.isFinal(targetClass.getModifiers()));
for (Advisor advisor : proxyFactory) {
factory.addAdvisors(advisor);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.security.authorization.method;

/**
* Marker interface implemented by Authorization proxies. Used to detect whether objects
* are AuthorizeReturnObject proxies.
*
* @author DingHao
* @since 6.4
* @see org.springframework.security.authorization.method.AuthorizeReturnObject
*/
public interface AuthorizationProxy {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.security.jackson2;

import java.io.IOException;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.security.authorization.method.AuthorizationProxy;
import org.springframework.security.authorization.method.AuthorizeReturnObject;

/**
* Serialize AuthorizationProxy objects generated by {@link AuthorizeReturnObject}
*
* @author DingHao
* @since 6.4
*/
public final class AuthorizationProxySerializer extends StdSerializer<AuthorizationProxy> {

public AuthorizationProxySerializer() {
super(AuthorizationProxy.class);
}

@Override
public void serialize(AuthorizationProxy value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
targetSerializer(serializers, value).serialize(value, gen, serializers);
}

private JsonSerializer<Object> targetSerializer(SerializerProvider serializers, AuthorizationProxy value)
throws JsonMappingException {
return serializers.findValueSerializer(AopProxyUtils.ultimateTargetClass(value));
}

@Override
public void serializeWithType(AuthorizationProxy value, JsonGenerator gen, SerializerProvider serializers,
TypeSerializer typeSer) throws IOException {
targetSerializer(serializers, value).serializeWithType(value, gen, serializers, typeSer);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ public class CoreJackson2Module extends SimpleModule {

public CoreJackson2Module() {
super(CoreJackson2Module.class.getName(), new Version(1, 0, 0, null, null, null));
addSerializer(new AuthorizationProxySerializer());
}

@Override
public void setupModule(SetupContext context) {
super.setupModule(context);
SecurityJackson2Modules.enableDefaultTyping(context.getOwner());
context.setMixInAnnotations(AnonymousAuthenticationToken.class, AnonymousAuthenticationTokenMixin.class);
context.setMixInAnnotations(RememberMeAuthenticationToken.class, RememberMeAuthenticationTokenMixin.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
import java.util.function.Supplier;
import java.util.stream.Stream;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;

Expand All @@ -46,6 +50,7 @@
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.jackson2.SecurityJackson2Modules;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
Expand Down Expand Up @@ -336,6 +341,34 @@ public void setTargetVisitorIgnoreValueTypesThenIgnores() {
assertThat(factory.proxy(35)).isEqualTo(35);
}

@Test
public void serializeAuthorizationProxyObjectWhenProvideJsonSerialize() throws JsonProcessingException {
SecurityContextHolder.getContext().setAuthentication(this.admin);
AuthorizationAdvisorProxyFactory factory = AuthorizationAdvisorProxyFactory.withDefaults();
JsonSerializeUser user1 = new JsonSerializeUser("used JsonSerialize annotation");
NoJsonSerializeUser user2 = new NoJsonSerializeUser("unused JsonSerialize annotation");

ObjectMapper mapper = new ObjectMapper();
mapper.registerModules(SecurityJackson2Modules.getModules(getClass().getClassLoader()));

assertThat(mapper.writeValueAsString(proxy(factory, user1))).doesNotContain("description");
assertThat(mapper.writeValueAsString(proxy(factory, user2))).contains("description");
}

@Test
public void serializeAuthorizationProxyObject() throws JsonProcessingException {
SecurityContextHolder.getContext().setAuthentication(this.admin);
AuthorizationAdvisorProxyFactory factory = AuthorizationAdvisorProxyFactory.withDefaults();
User user = proxy(factory, this.alan);
ObjectMapper mapper = new ObjectMapper();
assertThatExceptionOfType(InvalidDefinitionException.class).isThrownBy(() -> mapper.writeValueAsString(user));

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModules(SecurityJackson2Modules.getModules(getClass().getClassLoader()));
String actual = objectMapper.writeValueAsString(user);
assertThat(actual).isInstanceOf(String.class);
}

private Authentication authenticated(String user, String... authorities) {
return TestAuthentication.authenticated(TestAuthentication.withUsername(user).authorities(authorities).build());
}
Expand Down Expand Up @@ -363,6 +396,37 @@ interface Identifiable {

}

@JsonSerialize(as = User.class)
public static class JsonSerializeUser extends User {

private final String description;

JsonSerializeUser(String description) {
super("alan", "alan", "turing");
this.description = description;
}

public String getDescription() {
return this.description;
}

}

public static class NoJsonSerializeUser extends User {

private final String description;

NoJsonSerializeUser(String description) {
super("alan", "alan", "turing");
this.description = description;
}

public String getDescription() {
return this.description;
}

}

public static class User implements Identifiable, Comparable<User> {

private final String id;
Expand Down
29 changes: 23 additions & 6 deletions docs/modules/ROOT/pages/servlet/authorization/method-security.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2235,29 +2235,46 @@ com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-refer
====

This is due to how Jackson works with CGLIB proxies.
To address this, add the following annotation to the top of the `User` class:
To address this, register the SecurityJackson2Modules.getModules(ClassLoader) with ObjectMapper

[source,java]
----
ObjectMapper mapper = new ObjectMapper();
ClassLoader loader = getClass().getClassLoader();
List<Module> modules = SecurityJackson2Modules.getModules(loader);
mapper.registerModules(modules);
----

If you are using Spring Boot, you can also publish module bean and add `AuthorizationProxySerializer`:

[tabs]
======
Java::
+
[source,java,role="primary"]
----
@JsonSerialize(as = User.class)
public class User {
@Bean
SimpleModule authorizationProxyModule() {
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(new AuthorizationProxySerializer());
return simpleModule;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@JsonSerialize(`as` = User::class)
class User
@Bean
fun authorizationProxyModule(): SimpleModule {
val simpleModule = SimpleModule()
simpleModule.addSerializer(AuthorizationProxySerializer())
return simpleModule
}
----
======


Finally, you will need to publish a <<custom_advice, custom interceptor>> to catch the `AccessDeniedException` thrown for each field, which you can do like so:

[tabs]
Expand Down

0 comments on commit a8e073e

Please sign in to comment.