Use Method Security Enhancements

This commit is contained in:
Josh Cummings
2024-03-01 13:41:16 -07:00
parent f95464af8d
commit c73ce7587d
12 changed files with 272 additions and 4 deletions

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 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(value = "hasAuthority('{value}:read')")
@HandleAuthorizationDenied(handlerClass = Null.class)
public @interface AuthorizeRead {
String value();
}

View File

@@ -16,12 +16,52 @@
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.authorization.method.AuthorizationAdvisorProxyFactory;
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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 Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() {
return (f) -> f.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static PrePostTemplateDefaults templateDefaults() {
return new PrePostTemplateDefaults();
}
@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

@@ -18,13 +18,18 @@ package example;
import java.time.Instant;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
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
@JsonSerialize(as = Message.class)
@AuthorizeReturnObject
public class Message {
@Id
@@ -64,6 +69,7 @@ public class Message {
this.created = created;
}
@AuthorizeRead("message")
public String getText() {
return this.text;
}
@@ -72,6 +78,7 @@ public class Message {
this.text = text;
}
@AuthorizeRead("message")
public String getSummary() {
return this.summary;
}

View File

@@ -0,0 +1,43 @@
/*
* 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.web.bind.annotation.GetMapping;
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);
}
}

View File

@@ -20,6 +20,7 @@ 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;
/**
@@ -28,6 +29,7 @@ import org.springframework.stereotype.Repository;
* @author Rob Winch
*/
@Repository
@AuthorizeReturnObject
public interface MessageRepository extends CrudRepository<Message, Long> {
@Query("select m from Message m where m.to.id = ?#{ authentication.name }")

View File

@@ -0,0 +1,31 @@
/*
* 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

@@ -16,6 +16,8 @@
package example;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@@ -25,6 +27,7 @@ import jakarta.persistence.Id;
* @author Rob Winch
*/
@Entity(name = "users")
@JsonSerialize(as = User.class, contentUsing = JsonSerializer.class)
public class User {
@Id
@@ -46,6 +49,7 @@ public class User {
this.id = id;
}
@AuthorizeRead("user")
public String getFirstName() {
return this.firstName;
}
@@ -54,6 +58,7 @@ public class User {
this.firstName = firstName;
}
@AuthorizeRead("user")
public String getLastName() {
return this.lastName;
}

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

@@ -22,9 +22,12 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.test.context.support.WithMockUser;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
/**
* @author Rob Winch
@@ -37,9 +40,47 @@ public class DataApplicationTests {
@Test
@WithMockUser("rob")
void findAllOnlyToCurrentUser() {
void findAllOnlyToCurrentUserCantReadMessage() {
List<Message> messages = this.repository.findAll();
assertThat(messages).hasSize(3);
for (Message message : messages) {
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(message::getSummary);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(message::getText);
}
}
@Test
@WithMockUser(username="rob", authorities="message:read")
void findAllOnlyToCurrentUserCanReadMessage() {
List<Message> messages = this.repository.findAll();
assertThat(messages).hasSize(3);
for (Message message : messages) {
assertThatNoException().isThrownBy(message::getSummary);
assertThatNoException().isThrownBy(message::getText);
}
}
@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();
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(user::getFirstName);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(user::getLastName);
}
}
@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();
assertThatNoException().isThrownBy(user::getFirstName);
assertThatNoException().isThrownBy(user::getLastName);
}
}
}