diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessService.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessService.java new file mode 100644 index 0000000..752d321 --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessService.java @@ -0,0 +1,152 @@ +package de.fd.fh.server.access; + +import de.fd.fh.server.access.events.AccountCreatedEvent; +import de.fd.fh.server.access.events.AccountDeletedEvent; +import de.fd.fh.server.user.UserId; +import de.fd.fh.shared.network.messages.LoginRequest; +import de.fd.fh.shared.network.messages.RegistrateRequest; +import lombok.RequiredArgsConstructor; +import org.bson.types.ObjectId; + +import java.util.Base64; +import java.util.Observable; + +import static spark.Spark.halt; + +@RequiredArgsConstructor +public class AccessService extends Observable +{ + private final AccessRepository accessRepository; + + public AccessToken before(final String path, final String token) { + System.out.println("Pfad: " + path); + if (!(path.equals("/accounts/login") + || path.equals("/accounts/registrate") + )) + { + final AccessToken accessToken = authenticate(token); + + if (accessToken == null) + { + halt(401); + } + return accessToken; + } + return null; + } + + private AccessToken authenticate(final String bearerToken) + { + return accessRepository.findByToken(bearerToken.substring("Bearer ".length())).getToken(); + } + + public boolean createPlayer(RegistrateRequest message) + { + System.out.println("createPlayer: " + message); + + if (userNameDoesNotExist(message.getUserName())) + { + System.out.println("Name does exist."); + return false; + } + + final Access access = new Access( + new ObjectId().toHexString(), + message.getUserName(), + message.getPassword(), + UserId.random(), + null, + Role.USER + ); + + accessRepository.save(access); + + setChanged(); + notifyObservers(new AccountCreatedEvent(access.getName(), + access.getUserId())); + + System.out.println("DBLogin: " + access); + + return true; + } + + private boolean userNameDoesNotExist(final String name) + { + final Access user = accessRepository.findByUserName(name); + return user != null; + } + + public boolean logout(final String header) + { + try + { + System.out.println("logout " + header); + + final Access access = accessRepository.findByToken(header.substring("Bearer ".length())); + + access.removeToken(); + + accessRepository.save(access); + + return true; + } catch (Exception e) + { + e.printStackTrace(); + + return false; + } + } + + public LoginRequest authorization(final String header) + { + System.out.println("authorization"); + final String auth = header.substring("Basic ".length()); + + try + { + byte[] message = Base64.getDecoder().decode(auth); + + String messageStr = new String(message); + String[] user_password = messageStr.split(":"); + + final Access access = accessRepository.findByUserName(user_password[0]); + + System.out.println(access.getName()); + if (user_password[1].equals(access.getPassword())) + { + access.setToken(AccessToken.of(access)); + accessRepository.save(access); + + final LoginRequest loginRequest = new LoginRequest(); + loginRequest.setUserId(access.getUserId().getIdentifier()); + loginRequest.setToken(access.getToken().getToken()); + loginRequest.setName(access.getName()); + + return loginRequest; + } + + return null; + } catch (Exception e) + { + e.printStackTrace(); + return null; + } + } + + public boolean deleteAccount(final UserId userId, final AccessToken token) + { + if (!token.getUserId().getIdentifier() + .equals(userId.getIdentifier())) + { + return false; + } + if (accessRepository.deleteLoginByUserId(userId).wasAcknowledged()) + { + setChanged(); + notifyObservers(new AccountDeletedEvent(userId)); + + return true; + } + return false; + } +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessToken.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessToken.java new file mode 100644 index 0000000..e65bdab --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessToken.java @@ -0,0 +1,62 @@ +package de.fd.fh.server.access; + +import de.fd.fh.server.user.UserId; +import dev.morphia.annotations.Embedded; +import dev.morphia.annotations.PrePersist; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Random; + +@Embedded +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class AccessToken +{ + private String token; + + private LocalDateTime createdDate; + + private Role role; + + @Embedded + private UserId userId; + + static AccessToken of(final Access access) + { + return new AccessToken( + generateToken(), + LocalDateTime.now(), + access.getRole(), + access.getUserId() + ); + } + + @PrePersist + void prePersist() + { + this.createdDate = LocalDateTime.now(); + } + + private static String generateToken() + { + final String upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + final String lower = upper.toLowerCase(); + final String numbers = "0123456789"; + final String alphabet = upper + lower + numbers; + + System.out.println("generate Security Token."); + + Random random = new Random(); + StringBuilder generatedString = new StringBuilder(); + for (int i = 0; i < 64; i++) { + generatedString.append(alphabet.charAt(random.nextInt(alphabet.length()))); + } + + System.out.println("Token: " + generatedString); + return generatedString.toString(); + } +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/events/AccountCreatedEvent.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/events/AccountCreatedEvent.java new file mode 100644 index 0000000..c5e1530 --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/events/AccountCreatedEvent.java @@ -0,0 +1,13 @@ +package de.fd.fh.server.access.events; + +import de.fd.fh.server.user.UserId; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class AccountCreatedEvent +{ + private final String name; + private final UserId userId; +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/events/AccountDeletedEvent.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/events/AccountDeletedEvent.java new file mode 100644 index 0000000..7667bcb --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/events/AccountDeletedEvent.java @@ -0,0 +1,12 @@ +package de.fd.fh.server.access.events; + +import de.fd.fh.server.user.UserId; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class AccountDeletedEvent +{ + private final UserId userId; +} diff --git a/fh.fd.ci.server/src/test/java/de/fd/fh/server/access/AccessServiceTest.java b/fh.fd.ci.server/src/test/java/de/fd/fh/server/access/AccessServiceTest.java new file mode 100644 index 0000000..073a84a --- /dev/null +++ b/fh.fd.ci.server/src/test/java/de/fd/fh/server/access/AccessServiceTest.java @@ -0,0 +1,245 @@ +package de.fd.fh.server.access; + +import com.mongodb.WriteResult; +import de.fd.fh.server.user.UserId; +import de.fd.fh.shared.network.messages.LoginRequest; +import de.fd.fh.shared.network.messages.RegistrateRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import spark.HaltException; + +import java.time.LocalDateTime; +import java.util.Base64; +import java.util.Observable; +import java.util.Observer; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.*; + +class AccessServiceTest implements Observer +{ + private Object event; + + @BeforeEach + void before() + { + this.event = null; + } + + @Test + void given_authenticatedUser_when_serverAuthenticateUser_should_authenticateUser() + { + final Access access = new Access( + "testId", + "testName", + "testPwd", + UserId.of("12345"), + new AccessToken( + "testToken", + LocalDateTime.now(), + Role.USER, + UserId.of("12345") + ), + Role.USER); + + final AccessRepository repository = mock(AccessRepository.class); + when(repository.findByToken(any())) + .thenReturn(access); + final String path = "/test/path"; + final String token = "testToken"; + + final AccessToken result = new AccessService(repository).before(path, token); + + assertThat("User is not authenticated", result, notNullValue()); + then(repository).should().findByToken(any()); + then(repository).shouldHaveNoMoreInteractions(); + } + + @Test + void given_notAuthenticatedUser_when_serverAuthenticateUser_should_denyUser() + { + final Access access = new Access( + "testId", + "testName", + "testPwd", + UserId.of("12345"), + null, + Role.USER); + + final AccessRepository repository = mock(AccessRepository.class); + when(repository.findByToken(any())) + .thenReturn(access); + final String path = "/test/path"; + final String token = "testToken"; + + assertThrows(HaltException.class, () ->new AccessService(repository).before(path, token)); + + then(repository).should().findByToken(any()); + then(repository).shouldHaveNoMoreInteractions(); + } + + @Test + void given_newUser_when_createUser_should_storeNewUser() + { + final RegistrateRequest request = + RegistrateRequest.of("testUser", "testPwd"); + + final AccessRepository repository = mock(AccessRepository.class); + when(repository.findByUserName(any())) + .thenReturn(null); + ArgumentCaptor accessCaptor = ArgumentCaptor.forClass(Access.class); + + final AccessService service = new AccessService(repository); + service.addObserver(this); + + final boolean result = service.createPlayer(request); + + assertTrue(result); + assertThat("No event thrown", this.event, notNullValue()); + + verify(repository).save(accessCaptor.capture()); + final Access createdAccess = accessCaptor.getValue(); + + assertNotNull(createdAccess, "No Access created"); + assertNotNull(createdAccess.get_id(), "No Id created"); + assertNotNull(createdAccess.getUserId(), "No UserId created"); + assertEquals("testUser", createdAccess.getName(), "Wrong Username"); + assertEquals("testPwd", createdAccess.getPassword(), "Wrong Password"); + assertEquals(Role.USER.name(), createdAccess.getRole().name(), "Should be USER"); + assertNull(createdAccess.getToken(), "User should not be logged in"); + + then(repository).should().findByUserName(any()); + then(repository).should().save(any(Access.class)); + then(repository).shouldHaveNoMoreInteractions(); + } + + @Test + void given_loggedInUser_when_logout_should_logoutUser() + { + final Access access = new Access( + "testId", + "testName", + "testPwd", + UserId.of("12345"), + null, + Role.USER + ); + final AccessToken token = AccessToken.of(access); + access.setToken(token); + + final AccessRepository repository = mock(AccessRepository.class); + when(repository.findByToken(any())) + .thenReturn(access); + ArgumentCaptor accessCaptor = ArgumentCaptor.forClass(Access.class); + + final AccessService service = new AccessService(repository); + + final boolean result = service.logout("testToken"); + + assertTrue(result); + + verify(repository).save(accessCaptor.capture()); + final Access createdAccess = accessCaptor.getValue(); + + assertNotNull(createdAccess, "No Access created"); + assertNotNull(createdAccess.get_id(), "No Id created"); + assertNotNull(createdAccess.getUserId(), "No UserId created"); + assertEquals("testName", createdAccess.getName(), "Wrong Username"); + assertEquals("testPwd", createdAccess.getPassword(), "Wrong Password"); + assertEquals(Role.USER.name(), createdAccess.getRole().name(), "Should be USER"); + assertNull(createdAccess.getToken(), "User should logged out"); + + then(repository).should().findByToken(any()); + then(repository).should().save(any(Access.class)); + then(repository).shouldHaveNoMoreInteractions(); + } + + @Test + void given_storedUser_when_loginUser_should_returnLoginRequest() + { + final byte[] message = Base64.getEncoder().encode("testName:testPassword".getBytes()); + final String header = "Basic " + new String(message); + final Access access = new Access( + "testId", + "testName", + "testPassword", + UserId.of("12345"), + null, + Role.USER + ); + final AccessRepository repository = mock(AccessRepository.class); + when(repository.findByUserName(any())) + .thenReturn(access); + + final LoginRequest result = new AccessService(repository).authorization(header); + + assertNotNull(result); + assertEquals(result.getName(), "testName", "Wrong UserName"); + assertEquals(result.getUserId(), "12345", "Wrong Password"); + assertNotNull(result.getToken(), "Not logged in"); + } + + @Test + void given_storedUserWithWrongPassword_when_loginUser_should_returnAccessDeny() + { + final byte[] message = Base64.getEncoder().encode("testName:testPassword".getBytes()); + final String header = "Basic " + new String(message); + final AccessRepository repository = mock(AccessRepository.class); + + final LoginRequest result = new AccessService(repository).authorization(header); + + assertNull(result, "Return LoginRequest but wrong permissions"); + } + + @Test + void given_storedUser_when_deleteUser_should_deleteUser() + { + final UserId userId = UserId.of("12345"); + final AccessToken token = new AccessToken(null, null, null, UserId.of("12345")); + final AccessRepository repository = mock(AccessRepository.class); + when(repository.deleteLoginByUserId(any(UserId.class))) + .thenReturn(new WriteResult(1, false, null)); + + final AccessService service = new AccessService(repository); + service.addObserver(this); + + final boolean result = service.deleteAccount(userId, token); + + assertTrue(result); + assertNotNull(event); + + then(repository).should().deleteLoginByUserId(any(UserId.class)); + then(repository).shouldHaveNoMoreInteractions(); + } + + @Test + void given_storedUser_when_deleteUserWithWrongPermission_should_doNothing() + { + final UserId userId = UserId.of("12345"); + final AccessToken token = new AccessToken(null, null, null, UserId.of("98765")); + final AccessRepository repository = mock(AccessRepository.class); + when(repository.deleteLoginByUserId(any(UserId.class))) + .thenReturn(new WriteResult(1, false, null)); + + final AccessService service = new AccessService(repository); + service.addObserver(this); + + final boolean result = service.deleteAccount(userId, token); + + assertFalse(result); + assertNull(event); + + then(repository).shouldHaveNoInteractions(); + } + + @Override + public void update(Observable o, Object arg) + { + this.event = arg; + } +} \ No newline at end of file