Add Spring Data AOT Sample

Closes gh-317
This commit is contained in:
Josh Cummings
2024-09-10 16:28:10 -06:00
parent d18cde37b4
commit 56033d76f8
24 changed files with 1008 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
/*
* 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 example;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authorization.method.HandleAuthorizationDenied;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAuthority('{value}:read')")
@HandleAuthorizationDenied(handlerClass = Null.class)
public @interface AuthorizeRead {
String value();
}

View File

@@ -0,0 +1,16 @@
package example;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
@ControllerAdvice
public class CsrfAdvice {
@ModelAttribute
public void writeHeader(CsrfToken token, HttpServletResponse response) {
response.addHeader(token.getHeaderName(), token.getToken());
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2023 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 example;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Role;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@SpringBootApplication
@EnableMethodSecurity
public class DataApplication {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static AnnotationTemplateExpressionDefaults templateDefaults() {
return new AnnotationTemplateExpressionDefaults();
}
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(
User.withDefaultPasswordEncoder()
.username("rob")
.password("password")
.authorities("message:read", "user:read")
.build(),
User.withDefaultPasswordEncoder()
.username("luke")
.password("password")
.authorities("message:read")
.build());
}
public static void main(String[] args) {
SpringApplication.run(DataApplication.class, args);
}
}

View File

@@ -0,0 +1,13 @@
package example;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
public class DataRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.resources().registerPattern("import.sql");
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2020 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 example;
import java.time.Instant;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import org.springframework.security.authorization.method.AuthorizeReturnObject;
@Entity
public class Message {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String text;
private String summary;
private Instant created = Instant.now();
@ManyToOne
private User to;
@AuthorizeReturnObject
public User getTo() {
return this.to;
}
public void setTo(User to) {
this.to = to;
}
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public Instant getCreated() {
return this.created;
}
public void setCreated(Instant created) {
this.created = created;
}
@AuthorizeRead("message")
public String getText() {
return this.text;
}
public void setText(String text) {
this.text = text;
}
@AuthorizeRead("message")
public String getSummary() {
return this.summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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 example;
import java.util.List;
import java.util.Optional;
import org.springframework.security.authorization.method.AuthorizationProxy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MessageController {
private final MessageRepository messages;
public MessageController(MessageRepository messages) {
this.messages = messages;
}
@GetMapping
List<Message> getMessages() {
return this.messages.findAll();
}
@GetMapping("/{id}")
Optional<Message> getMessages(Long id) {
return this.messages.findById(id);
}
@PutMapping("/{id}")
Optional<Message> updateMessage(@PathVariable("id") Long id, @RequestBody String text) {
return this.messages.findById(id)
.map((message) -> {
message.setText(text);
// unwrap authorization proxy so Spring Data can persist
if (message instanceof AuthorizationProxy proxy) {
message = (Message) proxy.toAuthorizedTarget();
}
return this.messages.save(message);
});
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2020 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 example;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.security.authorization.method.AuthorizeReturnObject;
import org.springframework.stereotype.Repository;
/**
* A repository for accessing {@link Message}s.
*
* @author Rob Winch
*/
@Repository
@AuthorizeReturnObject
public interface MessageRepository extends CrudRepository<Message, Long> {
@Query("select m from Message m where m.to.id = ?#{ authentication.name }")
List<Message> findAll();
}

View File

@@ -0,0 +1,33 @@
/*
* 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 example;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.stereotype.Component;
@Component
public class Null implements MethodAuthorizationDeniedHandler {
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return null;
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2020 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 example;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
/**
* A user.
*
* @author Rob Winch
*/
@Entity(name = "users")
public class User {
@Id
private String id;
private String firstName;
private String lastName;
private String email;
private String password;
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
@AuthorizeRead("user")
public String getFirstName() {
return this.firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
@AuthorizeRead("user")
public String getLastName() {
return this.lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return this.email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@@ -0,0 +1,2 @@
org.springframework.aot.hint.RuntimeHintsRegistrar=\
example.DataRuntimeHintsRegistrar

View File

@@ -0,0 +1,17 @@
#
# 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.
#
spring.jackson.default-property-inclusion=non_null

View File

@@ -0,0 +1,10 @@
insert into users (id,email,password,first_name,last_name) values ('rob','rob@example.com','password','Rob','Winch');
insert into users (id,email,password,first_name,last_name) values ('luke','luke@example.com','password','Luke','Taylor');
insert into message (id,created,to_id,summary,text) values (100,'2014-07-10 10:00:00','rob','Hello Rob','This message is for Rob');
insert into message (id,created,to_id,summary,text) values (101,'2014-07-10 14:00:00','rob','How are you Rob?','This message is for Rob');
insert into message (id,created,to_id,summary,text) values (102,'2014-07-11 22:00:00','rob','Is this secure?','This message is for Rob');
insert into message (id,created,to_id,summary,text) values (110,'2014-07-12 10:00:00','luke','Hello Luke','This message is for Luke');
insert into message (id,created,to_id,summary,text) values (111,'2014-07-12 10:00:00','luke','Greetings Luke','This message is for Luke');
insert into message (id,created,to_id,summary,text) values (112,'2014-07-12 10:00:00','luke','Is this secure?','This message is for Luke');

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2020 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 example;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
*/
@SpringBootTest
public class DataApplicationTests {
@Autowired
MessageRepository repository;
@Test
@WithMockUser("rob")
void findAllOnlyToCurrentUserCantReadMessage() {
List<Message> messages = this.repository.findAll();
assertThat(messages).hasSize(3);
for (Message message : messages) {
assertThat(message.getSummary()).isNull();
assertThat(message.getText()).isNull();
}
}
@Test
@WithMockUser(username = "rob", authorities = "message:read")
void findAllOnlyToCurrentUserCanReadMessage() {
List<Message> messages = this.repository.findAll();
assertThat(messages).hasSize(3);
for (Message message : messages) {
assertThat(message.getSummary()).isNotNull();
assertThat(message.getText()).isNotNull();
}
}
@Test
@WithMockUser(username = "rob", authorities = "message:read")
void findAllOnlyToCurrentUserCantReadUserDetails() {
List<Message> messages = this.repository.findAll();
assertThat(messages).hasSize(3);
for (Message message : messages) {
User user = message.getTo();
assertThat(user.getFirstName()).isNull();
assertThat(user.getLastName()).isNull();
}
}
@Test
@WithMockUser(username = "rob", authorities = { "message:read", "user:read" })
void findAllOnlyToCurrentUserCanReadUserDetails() {
List<Message> messages = this.repository.findAll();
assertThat(messages).hasSize(3);
for (Message message : messages) {
User user = message.getTo();
assertThat(user.getFirstName()).isNotNull();
assertThat(user.getLastName()).isNotNull();
}
}
}