diff --git a/code/gms-backend.code-workspace b/code/gms-backend.code-workspace index 95fd7fe6..39fc0e1c 100644 --- a/code/gms-backend.code-workspace +++ b/code/gms-backend.code-workspace @@ -12,6 +12,7 @@ "java.configuration.updateBuildConfiguration": "automatic", "jest.disabledWorkspaceFolders": [ "gms-backend" - ] + ], + "java.debug.settings.onBuildFailureProceed": true } } \ No newline at end of file diff --git a/code/gms-backend/src/main/java/io/github/gms/auth/ldap/LdapUserAuthServiceImpl.java b/code/gms-backend/src/main/java/io/github/gms/auth/ldap/LdapUserAuthServiceImpl.java index 714afc8d..5470b824 100644 --- a/code/gms-backend/src/main/java/io/github/gms/auth/ldap/LdapUserAuthServiceImpl.java +++ b/code/gms-backend/src/main/java/io/github/gms/auth/ldap/LdapUserAuthServiceImpl.java @@ -1,53 +1,33 @@ package io.github.gms.auth.ldap; import static io.github.gms.common.util.Constants.CONFIG_AUTH_TYPE_LDAP; -import static io.github.gms.common.util.Constants.LDAP_CRYPT_PREFIX; -import java.time.Clock; -import java.time.ZonedDateTime; import java.util.List; -import java.util.stream.Collectors; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.query.LdapQueryBuilder; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import io.github.gms.auth.UserAuthService; import io.github.gms.auth.model.GmsUserDetails; -import io.github.gms.common.enums.EntityStatus; -import io.github.gms.secure.entity.UserEntity; -import io.github.gms.secure.repository.UserRepository; -import lombok.extern.slf4j.Slf4j; /** * @author Peter Szrnka * @since 1.0 */ -@Slf4j @Service @Profile(value = { CONFIG_AUTH_TYPE_LDAP }) public class LdapUserAuthServiceImpl implements UserAuthService { - private final Clock clock; - private final UserRepository repository; private final LdapTemplate ldapTemplate; + private final LdapUserPersistenceService ldapUserPersistenceService; - private final boolean storeLdapCredential; - - public LdapUserAuthServiceImpl( - Clock clock, - UserRepository repository, - LdapTemplate ldapTemplate, - @Value("${config.store.ldap.credential:false}") boolean storeLdapCredential) { - this.clock = clock; - this.repository = repository; + public LdapUserAuthServiceImpl(LdapTemplate ldapTemplate, LdapUserPersistenceService ldapUserPersistenceService) { this.ldapTemplate = ldapTemplate; - this.storeLdapCredential = storeLdapCredential; + this.ldapUserPersistenceService = ldapUserPersistenceService; } @Override @@ -59,43 +39,6 @@ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundEx throw new UsernameNotFoundException("User not found!"); } - GmsUserDetails foundUser = result.get(0); - - repository.findByUsername(username).ifPresentOrElse(userEntity -> saveExistingUser(foundUser, userEntity), - () -> saveNewUser(foundUser)); - return foundUser; - } - - private void saveExistingUser(GmsUserDetails foundUser, UserEntity userEntity) { - foundUser.setUserId(userEntity.getId()); - - if (storeLdapCredential && !userEntity.getCredential().equals(foundUser.getCredential())) { - userEntity.setCredential(getCredential(foundUser)); - repository.save(userEntity); - log.info("Credential has been updated for user={}", foundUser.getUsername()); - } - } - - private void saveNewUser(GmsUserDetails foundUser) { - UserEntity userEntity = new UserEntity(); - userEntity.setStatus(EntityStatus.ACTIVE); - userEntity.setName(foundUser.getName()); - userEntity.setUsername(foundUser.getUsername()); - userEntity.setCredential(getCredential(foundUser)); - userEntity.setCreationDate(ZonedDateTime.now(clock)); - userEntity.setEmail(foundUser.getEmail()); - userEntity.setRoles(foundUser.getAuthorities().stream().map(GrantedAuthority::getAuthority) - .collect(Collectors.joining(","))); - userEntity.setMfaEnabled(foundUser.isMfaEnabled()); - userEntity.setMfaSecret(foundUser.getMfaSecret()); - userEntity = repository.save(userEntity); - - foundUser.setUserId(userEntity.getId()); - log.info("User data has been saved into DB for user={}", foundUser.getUsername()); - } - - private String getCredential(GmsUserDetails foundUser) { - return storeLdapCredential ? foundUser.getCredential().replace(LDAP_CRYPT_PREFIX, "") - : "*PROVIDED_BY_LDAP*"; + return ldapUserPersistenceService.saveUserIfRequired(username, result.get(0)); } -} +} \ No newline at end of file diff --git a/code/gms-backend/src/main/java/io/github/gms/auth/ldap/LdapUserPersistenceService.java b/code/gms-backend/src/main/java/io/github/gms/auth/ldap/LdapUserPersistenceService.java new file mode 100644 index 00000000..9fbe2d6c --- /dev/null +++ b/code/gms-backend/src/main/java/io/github/gms/auth/ldap/LdapUserPersistenceService.java @@ -0,0 +1,12 @@ +package io.github.gms.auth.ldap; + +import io.github.gms.auth.model.GmsUserDetails; + +/** + * @author Peter Szrnka + * @since 1.0 + */ +public interface LdapUserPersistenceService { + + GmsUserDetails saveUserIfRequired(String username, GmsUserDetails foundUser); +} \ No newline at end of file diff --git a/code/gms-backend/src/main/java/io/github/gms/auth/ldap/LdapUserPersistenceServiceImpl.java b/code/gms-backend/src/main/java/io/github/gms/auth/ldap/LdapUserPersistenceServiceImpl.java new file mode 100644 index 00000000..bc608e87 --- /dev/null +++ b/code/gms-backend/src/main/java/io/github/gms/auth/ldap/LdapUserPersistenceServiceImpl.java @@ -0,0 +1,80 @@ +package io.github.gms.auth.ldap; + +import static io.github.gms.common.util.Constants.CONFIG_AUTH_TYPE_LDAP; +import static io.github.gms.common.util.Constants.LDAP_CRYPT_PREFIX; + +import java.time.Clock; +import java.time.ZonedDateTime; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Service; + +import io.github.gms.auth.model.GmsUserDetails; +import io.github.gms.common.enums.EntityStatus; +import io.github.gms.secure.entity.UserEntity; +import io.github.gms.secure.repository.UserRepository; +import lombok.extern.slf4j.Slf4j; + +/** + * @author Peter Szrnka + * @since 1.0 + */ +@Slf4j +@Service +@Profile(value = { CONFIG_AUTH_TYPE_LDAP }) +public class LdapUserPersistenceServiceImpl implements LdapUserPersistenceService { + + private final Clock clock; + private final UserRepository repository; + private final boolean storeLdapCredential; + + public LdapUserPersistenceServiceImpl(Clock clock, UserRepository repository, @Value("${config.store.ldap.credential:false}") boolean storeLdapCredential) { + this.clock = clock; + this.repository = repository; + this.storeLdapCredential = storeLdapCredential; + } + + @Override + public GmsUserDetails saveUserIfRequired(String username, GmsUserDetails foundUser) { + repository.findByUsername(username).ifPresentOrElse(userEntity -> saveExistingUser(foundUser, userEntity), + () -> saveNewUser(foundUser)); + + return foundUser; + } + + private void saveExistingUser(GmsUserDetails foundUser, UserEntity userEntity) { + foundUser.setUserId(userEntity.getId()); + + if (storeLdapCredential && !userEntity.getCredential().equals(foundUser.getCredential())) { + userEntity.setCredential(getCredential(foundUser)); + repository.save(userEntity); + log.info("Credential has been updated for user={}", foundUser.getUsername()); + } + } + + private void saveNewUser(GmsUserDetails foundUser) { + UserEntity userEntity = new UserEntity(); + userEntity.setStatus(EntityStatus.ACTIVE); + userEntity.setName(foundUser.getName()); + userEntity.setUsername(foundUser.getUsername()); + userEntity.setCredential(getCredential(foundUser)); + userEntity.setCreationDate(ZonedDateTime.now(clock)); + userEntity.setEmail(foundUser.getEmail()); + userEntity.setRoles(foundUser.getAuthorities().stream().map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(","))); + userEntity.setMfaEnabled(foundUser.isMfaEnabled()); + userEntity.setMfaSecret(foundUser.getMfaSecret()); + userEntity = repository.save(userEntity); + + foundUser.setUserId(userEntity.getId()); + log.info("User data has been saved into DB for user={}", foundUser.getUsername()); + } + + private String getCredential(GmsUserDetails foundUser) { + return storeLdapCredential ? foundUser.getCredential().replace(LDAP_CRYPT_PREFIX, "") + : "*PROVIDED_BY_LDAP*"; + } +} \ No newline at end of file diff --git a/code/gms-backend/src/test/java/io/github/gms/auth/ldap/LdapUserAuthServiceImplTest.java b/code/gms-backend/src/test/java/io/github/gms/auth/ldap/LdapUserAuthServiceImplTest.java index b095b2f9..76c10d00 100644 --- a/code/gms-backend/src/test/java/io/github/gms/auth/ldap/LdapUserAuthServiceImplTest.java +++ b/code/gms-backend/src/test/java/io/github/gms/auth/ldap/LdapUserAuthServiceImplTest.java @@ -4,24 +4,16 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.time.Clock; -import java.time.Instant; -import java.time.ZoneOffset; import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; -import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.springframework.ldap.core.AttributesMapper; import org.springframework.ldap.core.LdapTemplate; @@ -29,14 +21,10 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.read.ListAppender; import io.github.gms.abstraction.AbstractUnitTest; import io.github.gms.auth.model.GmsUserDetails; import io.github.gms.common.enums.MdcParameter; -import io.github.gms.secure.entity.UserEntity; -import io.github.gms.secure.repository.UserRepository; +import io.github.gms.util.DemoData; import io.github.gms.util.TestUtils; /** @@ -45,36 +33,16 @@ */ class LdapUserAuthServiceImplTest extends AbstractUnitTest { - private ListAppender logAppender; - private Clock clock; - private UserRepository repository; private LdapTemplate ldapTemplate; + private LdapUserPersistenceService ldapUserPersistenceService; private LdapUserAuthServiceImpl service; @BeforeEach public void setup() { MDC.put(MdcParameter.USER_ID.getDisplayName(), "1"); - - logAppender = new ListAppender<>(); - logAppender.start(); - } - - @BeforeEach - void beforeEach() { - logAppender = new ListAppender<>(); - logAppender.start(); - - clock = mock(Clock.class); - repository = mock(UserRepository.class); ldapTemplate = mock(LdapTemplate.class); - service = new LdapUserAuthServiceImpl(clock, repository, ldapTemplate, false); - ((Logger) LoggerFactory.getLogger(LdapUserAuthServiceImpl.class)).addAppender(logAppender); - } - - @AfterEach - public void teardown() { - logAppender.list.clear(); - logAppender.stop(); + ldapUserPersistenceService = mock(LdapUserPersistenceService.class); + service = new LdapUserAuthServiceImpl(ldapTemplate, ldapUserPersistenceService); } @Test @@ -87,6 +55,7 @@ void shouldNotFoundUser() { UsernameNotFoundException exception = assertThrows(UsernameNotFoundException.class, () -> service.loadUserByUsername("test")); // assert + verify(ldapTemplate).search(any(LdapQuery.class), any(AttributesMapper.class)); assertEquals("User not found!", exception.getMessage()); } @@ -100,93 +69,25 @@ void shouldFoundMoreUser() { UsernameNotFoundException exception = assertThrows(UsernameNotFoundException.class, () -> service.loadUserByUsername("test")); // assert + verify(ldapTemplate).search(any(LdapQuery.class), any(AttributesMapper.class)); assertEquals("User not found!", exception.getMessage()); } @Test @SuppressWarnings("unchecked") - void shouldNotUpdateCredentials() { - // arrange - when(ldapTemplate.search(any(LdapQuery.class), any(AttributesMapper.class))).thenReturn(List.of(TestUtils.createGmsUser())); - when(repository.findByUsername("test")).thenReturn(Optional.of(TestUtils.createUser())); - - // act - UserDetails response = service.loadUserByUsername("test"); - - // assert - assertNotNull(response); - } - - @Test - @SuppressWarnings("unchecked") - void shouldNotUpdateCredentialsWhenMatching() { - // arrange - service = new LdapUserAuthServiceImpl(clock, repository, ldapTemplate, true); - - GmsUserDetails userDetails = TestUtils.createGmsUser(); - userDetails.setCredential("test-credential"); - when(ldapTemplate.search(any(LdapQuery.class), any(AttributesMapper.class))).thenReturn(List.of(userDetails)); - - UserEntity entity = TestUtils.createUser(); - entity.setCredential("test-credential"); - when(repository.findByUsername("test")).thenReturn(Optional.of(entity)); - - // act - UserDetails response = service.loadUserByUsername("test"); - - // assert - assertNotNull(response); - verify(repository, never()).save(any(UserEntity.class)); - } - - @ParameterizedTest - @ValueSource(booleans = { true, false }) - @SuppressWarnings("unchecked") - void shouldUpdateCredentials(boolean storeLdapCredential) { - // arrange - service = new LdapUserAuthServiceImpl(clock, repository, ldapTemplate, storeLdapCredential); - - when(ldapTemplate.search(any(LdapQuery.class), any(AttributesMapper.class))).thenReturn(List.of(TestUtils.createGmsUser())); - when(repository.findByUsername("test")).thenReturn(Optional.of(TestUtils.createUser())); - - // act - UserDetails response = service.loadUserByUsername("test"); - - // assert - assertNotNull(response); - - if (storeLdapCredential) { - ArgumentCaptor userEntityCaptor = ArgumentCaptor.forClass(UserEntity.class); - verify(repository).save(userEntityCaptor.capture()); - UserEntity capturedUserEntity = userEntityCaptor.getValue(); - assertEquals("UserEntity(id=1, name=name, username=username, email=a@b.com, status=ACTIVE, credential=test, creationDate=null, roles=ROLE_USER, mfaEnabled=false, mfaSecret=null)", capturedUserEntity.toString()); - TestUtils.assertLogContains(logAppender, "Credential has been updated for user="); - } - } - - @Test - @SuppressWarnings("unchecked") - void shouldSaveNewLdapUser() { + void shouldFoundOneUser() { // arrange - when(clock.instant()).thenReturn(Instant.parse("2023-06-29T00:00:00Z")); - when(clock.getZone()).thenReturn(ZoneOffset.UTC); - service = new LdapUserAuthServiceImpl(clock, repository, ldapTemplate, false); - when(ldapTemplate.search(any(LdapQuery.class), any(AttributesMapper.class))).thenReturn(List.of(TestUtils.createGmsUser())); - when(repository.findByUsername("test")).thenReturn(Optional.empty()); - when(repository.save(any(UserEntity.class))).thenReturn(TestUtils.createUser()); + GmsUserDetails mockUser = TestUtils.createGmsUser(); + when(ldapTemplate.search(any(LdapQuery.class), any(AttributesMapper.class))).thenReturn(List.of(mockUser)); + when(ldapUserPersistenceService.saveUserIfRequired(anyString(), eq(mockUser))).thenReturn(mockUser); // act UserDetails response = service.loadUserByUsername("test"); // assert assertNotNull(response); - assertEquals("GmsUserDetails(name=username1, email=a@b.com, userId=1, username=username1, credential=test, authorities=[ROLE_USER], accountNonLocked=true, enabled=true, mfaEnabled=false, mfaSecret=MFA_SECRET)", response.toString()); - - ArgumentCaptor userEntityCaptor = ArgumentCaptor.forClass(UserEntity.class); - verify(repository).save(userEntityCaptor.capture()); - - UserEntity capturedUserEntity = userEntityCaptor.getValue(); - assertEquals("UserEntity(id=null, name=username1, username=username1, email=a@b.com, status=ACTIVE, credential=*PROVIDED_BY_LDAP*, creationDate=2023-06-29T00:00Z, roles=ROLE_USER, mfaEnabled=false, mfaSecret=MFA_SECRET)", capturedUserEntity.toString()); - TestUtils.assertLogContains(logAppender, "User data has been saved into DB for user="); + assertEquals(DemoData.USERNAME1, response.getUsername()); + verify(ldapTemplate).search(any(LdapQuery.class), any(AttributesMapper.class)); + verify(ldapUserPersistenceService).saveUserIfRequired(anyString(), eq(mockUser)); } } diff --git a/code/gms-backend/src/test/java/io/github/gms/auth/ldap/LdapUserPersistenceServiceImplTest.java b/code/gms-backend/src/test/java/io/github/gms/auth/ldap/LdapUserPersistenceServiceImplTest.java new file mode 100644 index 00000000..05549542 --- /dev/null +++ b/code/gms-backend/src/test/java/io/github/gms/auth/ldap/LdapUserPersistenceServiceImplTest.java @@ -0,0 +1,144 @@ +package io.github.gms.auth.ldap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.slf4j.LoggerFactory; +import org.springframework.ldap.core.AttributesMapper; +import org.springframework.ldap.query.LdapQuery; +import org.springframework.security.core.userdetails.UserDetails; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import io.github.gms.abstraction.AbstractUnitTest; +import io.github.gms.auth.model.GmsUserDetails; +import io.github.gms.secure.entity.UserEntity; +import io.github.gms.secure.repository.UserRepository; +import io.github.gms.util.TestUtils; + +/** + * Unit test of {@link LdapUserPersistenceServiceImpl} + * + * @author Peter Szrnka + * @since 1.0 + */ +class LdapUserPersistenceServiceImplTest extends AbstractUnitTest { + + private ListAppender logAppender; + private Clock clock; + private UserRepository repository; + private LdapUserPersistenceServiceImpl service; + + @BeforeEach + void beforeEach() { + logAppender = new ListAppender<>(); + logAppender.start(); + + clock = mock(Clock.class); + repository = mock(UserRepository.class); + service = new LdapUserPersistenceServiceImpl(clock, repository, false); + ((Logger) LoggerFactory.getLogger(LdapUserPersistenceServiceImpl.class)).addAppender(logAppender); + } + + @AfterEach + public void teardown() { + logAppender.list.clear(); + logAppender.stop(); + } + + @Test + void shouldNotUpdateCredentials() { + // arrange + when(repository.findByUsername("test")).thenReturn(Optional.of(TestUtils.createUser())); + + // act + UserDetails response = service.saveUserIfRequired("test", TestUtils.createGmsUser()); + + // assert + assertNotNull(response); + } + + @Test + void shouldNotUpdateCredentialsWhenMatching() { + // arrange + service = new LdapUserPersistenceServiceImpl(clock, repository, true); + + GmsUserDetails userDetails = TestUtils.createGmsUser(); + userDetails.setCredential("test-credential"); + + UserEntity entity = TestUtils.createUser(); + entity.setCredential("test-credential"); + when(repository.findByUsername("test")).thenReturn(Optional.of(entity)); + + // act + UserDetails response = service.saveUserIfRequired("test", userDetails); + + + // assert + assertNotNull(response); + verify(repository, never()).save(any(UserEntity.class)); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void shouldUpdateCredentials(boolean storeLdapCredential) { + // arrange + service = new LdapUserPersistenceServiceImpl(clock, repository, storeLdapCredential); + when(repository.findByUsername("test")).thenReturn(Optional.of(TestUtils.createUser())); + + // act + UserDetails response = service.saveUserIfRequired("test", TestUtils.createGmsUser()); + + // assert + assertNotNull(response); + + if (storeLdapCredential) { + ArgumentCaptor userEntityCaptor = ArgumentCaptor.forClass(UserEntity.class); + verify(repository).save(userEntityCaptor.capture()); + UserEntity capturedUserEntity = userEntityCaptor.getValue(); + assertEquals("UserEntity(id=1, name=name, username=username, email=a@b.com, status=ACTIVE, credential=test, creationDate=null, roles=ROLE_USER, mfaEnabled=false, mfaSecret=null)", capturedUserEntity.toString()); + TestUtils.assertLogContains(logAppender, "Credential has been updated for user="); + } + } + + @Test + void shouldSaveNewLdapUser() { + // arrange + when(clock.instant()).thenReturn(Instant.parse("2023-06-29T00:00:00Z")); + when(clock.getZone()).thenReturn(ZoneOffset.UTC); + when(repository.findByUsername("test")).thenReturn(Optional.empty()); + when(repository.save(any(UserEntity.class))).thenReturn(TestUtils.createUser()); + + // act + UserDetails response = service.saveUserIfRequired("test", TestUtils.createGmsUser()); + + // assert + assertNotNull(response); + assertEquals("GmsUserDetails(name=username1, email=a@b.com, userId=1, username=username1, credential=test, authorities=[ROLE_USER], accountNonLocked=true, enabled=true, mfaEnabled=false, mfaSecret=MFA_SECRET)", response.toString()); + + ArgumentCaptor userEntityCaptor = ArgumentCaptor.forClass(UserEntity.class); + verify(repository).save(userEntityCaptor.capture()); + + UserEntity capturedUserEntity = userEntityCaptor.getValue(); + assertEquals("UserEntity(id=null, name=username1, username=username1, email=a@b.com, status=ACTIVE, credential=*PROVIDED_BY_LDAP*, creationDate=2023-06-29T00:00Z, roles=ROLE_USER, mfaEnabled=false, mfaSecret=MFA_SECRET)", capturedUserEntity.toString()); + TestUtils.assertLogContains(logAppender, "User data has been saved into DB for user="); + } +} \ No newline at end of file