diff --git a/core/src/main/java/org/springframework/security/core/userdetails/UserAuthorities.java b/core/src/main/java/org/springframework/security/core/userdetails/UserAuthorities.java new file mode 100644 index 00000000000..a470f2d6ba1 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/userdetails/UserAuthorities.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-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.core.userdetails; + +import java.io.Serializable; +import java.util.Collection; + +import org.springframework.security.core.GrantedAuthority; + +/** + * Represents user authorities. This interface is mostly intended for scenarios where a + * password is not need, like X509, CAS, Passkeys, One Time Tokens and others. + * + * @author Marcus da Coregio + * @since 6.4 + * @see UserAuthoritiesRepository + * @see UserDetails + */ +public interface UserAuthorities extends Serializable { + + /** + * Returns the authorities granted to the user. Cannot return null. + * @return the authorities, sorted by natural key (never null) + */ + Collection getAuthorities(); + + /** + * Returns the username used to authenticate the user. Cannot return + * null. + * @return the username (never null) + */ + String getUsername(); + +} diff --git a/core/src/main/java/org/springframework/security/core/userdetails/UserAuthoritiesRepository.java b/core/src/main/java/org/springframework/security/core/userdetails/UserAuthoritiesRepository.java new file mode 100644 index 00000000000..07d4f836009 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/userdetails/UserAuthoritiesRepository.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-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.core.userdetails; + +/** + * Repository interface for accessing user authorities. + * + * @author Marcus da Coregio + * @since 6.4 + * @see UserAuthorities + */ +public interface UserAuthoritiesRepository { + + /** + * Finds the authorities associated with the given username. + * @param username the username for which to find authorities + * @return the {@link UserAuthorities} object containing authorities associated with + * the specified username + * @throws UsernameNotFoundException if the user could not be found or the user has no + * GrantedAuthority + */ + UserAuthorities findAuthoritiesByUsername(String username) throws UsernameNotFoundException; + +} diff --git a/core/src/main/java/org/springframework/security/core/userdetails/UserDetails.java b/core/src/main/java/org/springframework/security/core/userdetails/UserDetails.java index bab08c5a2ac..485e382c4d1 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/UserDetails.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/UserDetails.java @@ -16,11 +16,7 @@ package org.springframework.security.core.userdetails; -import java.io.Serializable; -import java.util.Collection; - import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; /** * Provides core user information. @@ -40,13 +36,7 @@ * @see UserDetailsService * @see UserCache */ -public interface UserDetails extends Serializable { - - /** - * Returns the authorities granted to the user. Cannot return null. - * @return the authorities, sorted by natural key (never null) - */ - Collection getAuthorities(); +public interface UserDetails extends UserAuthorities { /** * Returns the password used to authenticate the user. @@ -54,13 +44,6 @@ public interface UserDetails extends Serializable { */ String getPassword(); - /** - * Returns the username used to authenticate the user. Cannot return - * null. - * @return the username (never null) - */ - String getUsername(); - /** * Indicates whether the user's account has expired. An expired account cannot be * authenticated. diff --git a/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsServiceAuthoritiesRepository.java b/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsServiceAuthoritiesRepository.java new file mode 100644 index 00000000000..8539d031343 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsServiceAuthoritiesRepository.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-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.core.userdetails; + +import org.springframework.util.Assert; + +/** + * An implementation of {@link UserAuthoritiesRepository} that uses a + * {@link UserDetailsService} to load the user authorities. + * + * @author Marcus da Coregio + * @since 6.4 + */ +public class UserDetailsServiceAuthoritiesRepository implements UserAuthoritiesRepository { + + private final UserDetailsService userDetailsService; + + public UserDetailsServiceAuthoritiesRepository(UserDetailsService userDetailsService) { + Assert.notNull(userDetailsService, "userDetailsService cannot be null"); + this.userDetailsService = userDetailsService; + } + + @Override + public UserAuthorities findAuthoritiesByUsername(String username) { + return this.userDetailsService.loadUserByUsername(username); + } + +} diff --git a/core/src/test/java/org/springframework/security/core/userdetails/UserDetailsServiceAuthoritiesRepositoryTests.java b/core/src/test/java/org/springframework/security/core/userdetails/UserDetailsServiceAuthoritiesRepositoryTests.java new file mode 100644 index 00000000000..2ae25a5471a --- /dev/null +++ b/core/src/test/java/org/springframework/security/core/userdetails/UserDetailsServiceAuthoritiesRepositoryTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-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.core.userdetails; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link UserDetailsServiceAuthoritiesRepository} + * + * @author Marcus da Coregio + */ +class UserDetailsServiceAuthoritiesRepositoryTests { + + UserDetailsService userDetailsService = new InMemoryUserDetailsManager(PasswordEncodedUser.user(), + PasswordEncodedUser.admin()); + + UserDetailsServiceAuthoritiesRepository userAuthoritiesRepository; + + @BeforeEach + void setup() { + this.userAuthoritiesRepository = new UserDetailsServiceAuthoritiesRepository(this.userDetailsService); + } + + @Test + void findUserAuthoritiesWhenUserExistsThenReturn() { + UserAuthorities admin = this.userAuthoritiesRepository.findAuthoritiesByUsername("admin"); + assertThat(admin.getAuthorities()).extracting(GrantedAuthority::getAuthority) + .containsExactly("ROLE_ADMIN", "ROLE_USER"); + } + + @Test + void findUserAuthoritiesWhenUserDoesNotExistsThenUsernameNotFoundException() { + assertThatExceptionOfType(UsernameNotFoundException.class) + .isThrownBy(() -> this.userAuthoritiesRepository.findAuthoritiesByUsername("unknown")); + } + +}