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
committed by marcel
parent fced33e033
commit 0b0559cbe9
2 changed files with 30 additions and 34 deletions

View File

@@ -29,24 +29,22 @@ public class CustomUserDetailsService implements UserDetailsService {
private final AppUserRepository userRepository; private final AppUserRepository userRepository;
@Override @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
AppUser appUser = userRepository.findByUsername(username) AppUser appUser = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User nicht gefunden: " + username)); .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() var authorities = appUser.getGroups().stream()
.flatMap(group -> group.getPermissions().stream()) .flatMap(group -> group.getPermissions().stream())
.peek(p -> { .peek(p -> {
if (!KNOWN_PERMISSIONS.contains(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) .map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
// Rückgabe des Standard Spring Security User Objekts
return new User( return new User(
appUser.getUsername(), appUser.getEmail(),
appUser.getPassword(), appUser.getPassword(),
appUser.isEnabled(), appUser.isEnabled(),
true, true, true, 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.AppUser;
import org.raddatz.familienarchiv.model.UserGroup; import org.raddatz.familienarchiv.model.UserGroup;
import org.raddatz.familienarchiv.repository.AppUserRepository; 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.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -29,40 +28,40 @@ class CustomUserDetailsServiceTest {
// ─── loadUserByUsername — not found ────────────────────────────────────── // ─── loadUserByUsername — not found ──────────────────────────────────────
@Test @Test
void loadUserByUsername_throwsUsernameNotFoundException_whenUserNotFound() { void loadUserByEmail_throwsUsernameNotFoundException_whenUserNotFound() {
when(userRepository.findByUsername("ghost")).thenReturn(Optional.empty()); when(userRepository.findByEmail("ghost@example.com")).thenReturn(Optional.empty());
assertThatThrownBy(() -> service.loadUserByUsername("ghost")) assertThatThrownBy(() -> service.loadUserByUsername("ghost@example.com"))
.isInstanceOf(UsernameNotFoundException.class) .isInstanceOf(UsernameNotFoundException.class)
.hasMessageContaining("ghost"); .hasMessageContaining("ghost@example.com");
} }
// ─── loadUserByUsername — happy path ───────────────────────────────────── // ─── loadUserByUsername — happy path ─────────────────────────────────────
@Test @Test
void loadUserByUsername_returnsUserDetails_withMappedAuthorities() { void loadUserByEmail_returnsUserDetails_withEmailAsPrincipal() {
UserGroup group = UserGroup.builder().id(UUID.randomUUID()).name("Admins") UserGroup group = UserGroup.builder().id(UUID.randomUUID()).name("Admins")
.permissions(Set.of("READ_ALL", "WRITE_ALL")).build(); .permissions(Set.of("READ_ALL", "WRITE_ALL")).build();
AppUser user = AppUser.builder().id(UUID.randomUUID()) 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(); .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") assertThat(details.getAuthorities()).extracting("authority")
.contains("READ_ALL", "WRITE_ALL"); .contains("READ_ALL", "WRITE_ALL");
} }
@Test @Test
void loadUserByUsername_returnsEmptyAuthorities_whenUserHasNoGroups() { void loadUserByEmail_returnsEmptyAuthorities_whenUserHasNoGroups() {
AppUser user = AppUser.builder().id(UUID.randomUUID()) 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(); .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(); assertThat(details.getAuthorities()).isEmpty();
} }
@@ -70,16 +69,15 @@ class CustomUserDetailsServiceTest {
// ─── loadUserByUsername — unknown permission ────────────────────────────── // ─── loadUserByUsername — unknown permission ──────────────────────────────
@Test @Test
void loadUserByUsername_grantsUnknownPermission_butLogsWarning() { void loadUserByEmail_grantsUnknownPermission_butLogsWarning() {
// Unknown permissions should still be granted (logged as warning, not silently dropped)
UserGroup group = UserGroup.builder().id(UUID.randomUUID()).name("CustomGroup") UserGroup group = UserGroup.builder().id(UUID.randomUUID()).name("CustomGroup")
.permissions(Set.of("UNKNOWN_CUSTOM_PERM")).build(); .permissions(Set.of("UNKNOWN_CUSTOM_PERM")).build();
AppUser user = AppUser.builder().id(UUID.randomUUID()) 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(); .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") assertThat(details.getAuthorities()).extracting("authority")
.contains("UNKNOWN_CUSTOM_PERM"); .contains("UNKNOWN_CUSTOM_PERM");
@@ -88,13 +86,13 @@ class CustomUserDetailsServiceTest {
// ─── loadUserByUsername — disabled user ─────────────────────────────────── // ─── loadUserByUsername — disabled user ───────────────────────────────────
@Test @Test
void loadUserByUsername_returnsDisabledUser_whenUserIsDisabled() { void loadUserByEmail_returnsDisabledUser_whenUserIsDisabled() {
AppUser user = AppUser.builder().id(UUID.randomUUID()) 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(); .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(); assertThat(details.isEnabled()).isFalse();
} }
@@ -102,17 +100,17 @@ class CustomUserDetailsServiceTest {
// ─── loadUserByUsername — multi-group permission merge ──────────────────── // ─── loadUserByUsername — multi-group permission merge ────────────────────
@Test @Test
void loadUserByUsername_mergesPermissionsFromMultipleGroups() { void loadUserByEmail_mergesPermissionsFromMultipleGroups() {
UserGroup g1 = UserGroup.builder().id(UUID.randomUUID()).name("Readers") UserGroup g1 = UserGroup.builder().id(UUID.randomUUID()).name("Readers")
.permissions(Set.of("READ_ALL")).build(); .permissions(Set.of("READ_ALL")).build();
UserGroup g2 = UserGroup.builder().id(UUID.randomUUID()).name("Writers") UserGroup g2 = UserGroup.builder().id(UUID.randomUUID()).name("Writers")
.permissions(Set.of("WRITE_ALL")).build(); .permissions(Set.of("WRITE_ALL")).build();
AppUser user = AppUser.builder().id(UUID.randomUUID()) 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(); .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") assertThat(details.getAuthorities()).extracting("authority")
.containsExactlyInAnyOrder("READ_ALL", "WRITE_ALL"); .containsExactlyInAnyOrder("READ_ALL", "WRITE_ALL");