feat(auth): switch CustomUserDetailsService to email-based lookup

loadUserByUsername now calls findByEmail and returns email as the
Spring Security principal name. Tests updated to assert email identity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-18 20:31:32 +02:00
parent 40db46945f
commit cf9a8ea393
2 changed files with 30 additions and 34 deletions

View File

@@ -29,24 +29,22 @@ public class CustomUserDetailsService implements UserDetailsService {
private final AppUserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AppUser appUser = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User nicht gefunden: " + username));
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
AppUser appUser = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User nicht gefunden: " + email));
// Collect all permissions from all groups; warn about any that don't match a known Permission enum value
var authorities = appUser.getGroups().stream()
.flatMap(group -> group.getPermissions().stream())
.peek(p -> {
if (!KNOWN_PERMISSIONS.contains(p)) {
log.warn("Unknown permission '{}' found in database for user '{}' — it will be granted but never matched by @RequirePermission", p, appUser.getUsername());
log.warn("Unknown permission '{}' found in database for user '{}' — it will be granted but never matched by @RequirePermission", p, appUser.getEmail());
}
})
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
// Rückgabe des Standard Spring Security User Objekts
return new User(
appUser.getUsername(),
appUser.getEmail(),
appUser.getPassword(),
appUser.isEnabled(),
true, true, true,

View File

@@ -8,7 +8,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.raddatz.familienarchiv.model.AppUser;
import org.raddatz.familienarchiv.model.UserGroup;
import org.raddatz.familienarchiv.repository.AppUserRepository;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -29,40 +28,40 @@ class CustomUserDetailsServiceTest {
// ─── loadUserByUsername — not found ──────────────────────────────────────
@Test
void loadUserByUsername_throwsUsernameNotFoundException_whenUserNotFound() {
when(userRepository.findByUsername("ghost")).thenReturn(Optional.empty());
void loadUserByEmail_throwsUsernameNotFoundException_whenUserNotFound() {
when(userRepository.findByEmail("ghost@example.com")).thenReturn(Optional.empty());
assertThatThrownBy(() -> service.loadUserByUsername("ghost"))
assertThatThrownBy(() -> service.loadUserByUsername("ghost@example.com"))
.isInstanceOf(UsernameNotFoundException.class)
.hasMessageContaining("ghost");
.hasMessageContaining("ghost@example.com");
}
// ─── loadUserByUsername — happy path ─────────────────────────────────────
@Test
void loadUserByUsername_returnsUserDetails_withMappedAuthorities() {
void loadUserByEmail_returnsUserDetails_withEmailAsPrincipal() {
UserGroup group = UserGroup.builder().id(UUID.randomUUID()).name("Admins")
.permissions(Set.of("READ_ALL", "WRITE_ALL")).build();
AppUser user = AppUser.builder().id(UUID.randomUUID())
.username("admin").password("hashed").enabled(true)
.email("admin@example.com").password("hashed").enabled(true)
.groups(Set.of(group)).build();
when(userRepository.findByUsername("admin")).thenReturn(Optional.of(user));
when(userRepository.findByEmail("admin@example.com")).thenReturn(Optional.of(user));
UserDetails details = service.loadUserByUsername("admin");
UserDetails details = service.loadUserByUsername("admin@example.com");
assertThat(details.getUsername()).isEqualTo("admin");
assertThat(details.getUsername()).isEqualTo("admin@example.com");
assertThat(details.getAuthorities()).extracting("authority")
.contains("READ_ALL", "WRITE_ALL");
}
@Test
void loadUserByUsername_returnsEmptyAuthorities_whenUserHasNoGroups() {
void loadUserByEmail_returnsEmptyAuthorities_whenUserHasNoGroups() {
AppUser user = AppUser.builder().id(UUID.randomUUID())
.username("viewer").password("hashed").enabled(true)
.email("viewer@example.com").password("hashed").enabled(true)
.groups(Set.of()).build();
when(userRepository.findByUsername("viewer")).thenReturn(Optional.of(user));
when(userRepository.findByEmail("viewer@example.com")).thenReturn(Optional.of(user));
UserDetails details = service.loadUserByUsername("viewer");
UserDetails details = service.loadUserByUsername("viewer@example.com");
assertThat(details.getAuthorities()).isEmpty();
}
@@ -70,16 +69,15 @@ class CustomUserDetailsServiceTest {
// ─── loadUserByUsername — unknown permission ──────────────────────────────
@Test
void loadUserByUsername_grantsUnknownPermission_butLogsWarning() {
// Unknown permissions should still be granted (logged as warning, not silently dropped)
void loadUserByEmail_grantsUnknownPermission_butLogsWarning() {
UserGroup group = UserGroup.builder().id(UUID.randomUUID()).name("CustomGroup")
.permissions(Set.of("UNKNOWN_CUSTOM_PERM")).build();
AppUser user = AppUser.builder().id(UUID.randomUUID())
.username("custom").password("hashed").enabled(true)
.email("custom@example.com").password("hashed").enabled(true)
.groups(Set.of(group)).build();
when(userRepository.findByUsername("custom")).thenReturn(Optional.of(user));
when(userRepository.findByEmail("custom@example.com")).thenReturn(Optional.of(user));
UserDetails details = service.loadUserByUsername("custom");
UserDetails details = service.loadUserByUsername("custom@example.com");
assertThat(details.getAuthorities()).extracting("authority")
.contains("UNKNOWN_CUSTOM_PERM");
@@ -88,13 +86,13 @@ class CustomUserDetailsServiceTest {
// ─── loadUserByUsername — disabled user ───────────────────────────────────
@Test
void loadUserByUsername_returnsDisabledUser_whenUserIsDisabled() {
void loadUserByEmail_returnsDisabledUser_whenUserIsDisabled() {
AppUser user = AppUser.builder().id(UUID.randomUUID())
.username("disabled").password("hashed").enabled(false)
.email("disabled@example.com").password("hashed").enabled(false)
.groups(Set.of()).build();
when(userRepository.findByUsername("disabled")).thenReturn(Optional.of(user));
when(userRepository.findByEmail("disabled@example.com")).thenReturn(Optional.of(user));
UserDetails details = service.loadUserByUsername("disabled");
UserDetails details = service.loadUserByUsername("disabled@example.com");
assertThat(details.isEnabled()).isFalse();
}
@@ -102,17 +100,17 @@ class CustomUserDetailsServiceTest {
// ─── loadUserByUsername — multi-group permission merge ────────────────────
@Test
void loadUserByUsername_mergesPermissionsFromMultipleGroups() {
void loadUserByEmail_mergesPermissionsFromMultipleGroups() {
UserGroup g1 = UserGroup.builder().id(UUID.randomUUID()).name("Readers")
.permissions(Set.of("READ_ALL")).build();
UserGroup g2 = UserGroup.builder().id(UUID.randomUUID()).name("Writers")
.permissions(Set.of("WRITE_ALL")).build();
AppUser user = AppUser.builder().id(UUID.randomUUID())
.username("multi").password("hashed").enabled(true)
.email("multi@example.com").password("hashed").enabled(true)
.groups(Set.of(g1, g2)).build();
when(userRepository.findByUsername("multi")).thenReturn(Optional.of(user));
when(userRepository.findByEmail("multi@example.com")).thenReturn(Optional.of(user));
UserDetails details = service.loadUserByUsername("multi");
UserDetails details = service.loadUserByUsername("multi@example.com");
assertThat(details.getAuthorities()).extracting("authority")
.containsExactlyInAnyOrder("READ_ALL", "WRITE_ALL");