Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature 633 admin can change email address of user #1029

Merged
merged 7 commits into from
Feb 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ private AdministrationAPIConstants() {

public static final String API_SHOW_USER_DETAILS = API_ADMINISTRATION + "user/{userId}";
public static final String API_DELETE_USER = API_ADMINISTRATION + "user/{userId}";
public static final String API_UPDATE_USER_EMAIL_ADDRESS = API_ADMINISTRATION + "user/{userId}/email/{newEmailAddress}";

public static final String API_SHOW_PROJECT_DETAILS = API_ADMINISTRATION + "project/{projectId}";
public static final String API_CHANGE_PROJECT_DETAILS = API_ADMINISTRATION + "project/{projectId}";
public static final String API_DELETE_PROJECT = API_ADMINISTRATION + "project/{projectId}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.mercedesbenz.sechub.sharedkernel.usecases.admin.user.UseCaseAdminListsAllUsers;
import com.mercedesbenz.sechub.sharedkernel.usecases.admin.user.UseCaseAdminRevokesAdminRightsFromAdmin;
import com.mercedesbenz.sechub.sharedkernel.usecases.admin.user.UseCaseAdminShowsUserDetails;
import com.mercedesbenz.sechub.sharedkernel.usecases.admin.user.UseCaseAdminUpdatesUserEmailAddress;

/**
* The rest api for user administration done by a super admin.
Expand Down Expand Up @@ -58,6 +59,9 @@ public class UserAdministrationRestController {
@Autowired
UserRevokeSuperAdminRightsService userRevokeSuperAdminRightsService;

@Autowired
UserEmailAddressUpdateService userEmailAddressUpdateService;

/* @formatter:off */
@UseCaseAdminAcceptsSignup(@Step(number=1,name="Rest call", description="Administrator accepts a persisted self registration entry by calling rest api",needsRestDoc=true))
@RequestMapping(path = AdministrationAPIConstants.API_ACCEPT_USER_SIGNUP, method = RequestMethod.POST, produces= {MediaType.APPLICATION_JSON_VALUE})
Expand Down Expand Up @@ -115,4 +119,12 @@ public void revokeSuperAdminrights(@PathVariable(name="userId") String userId) {
userRevokeSuperAdminRightsService.revokeSuperAdminRightsFrom(userId);
}

/* @formatter:off */
@UseCaseAdminUpdatesUserEmailAddress(@Step(number=1,name="Rest call",description="User emaill address will be changed",needsRestDoc=true))
@RequestMapping(path = AdministrationAPIConstants.API_UPDATE_USER_EMAIL_ADDRESS, method = RequestMethod.PUT, produces= {MediaType.APPLICATION_JSON_VALUE})
public void updateUserEmailAdderss(@PathVariable(name="userId") String userId,@PathVariable(name="newEmailAddress") String newEmailAddress) {
/* @formatter:on */
userEmailAddressUpdateService.updateUserEmailAddress(userId, newEmailAddress);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: MIT
package com.mercedesbenz.sechub.domain.administration.user;

import javax.annotation.security.RolesAllowed;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import com.mercedesbenz.sechub.sharedkernel.RoleConstants;
import com.mercedesbenz.sechub.sharedkernel.Step;
import com.mercedesbenz.sechub.sharedkernel.error.NotAcceptableException;
import com.mercedesbenz.sechub.sharedkernel.logging.AuditLogService;
import com.mercedesbenz.sechub.sharedkernel.logging.LogSanitizer;
import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessage;
import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessageService;
import com.mercedesbenz.sechub.sharedkernel.messaging.IsSendingAsyncMessage;
import com.mercedesbenz.sechub.sharedkernel.messaging.MessageDataKeys;
import com.mercedesbenz.sechub.sharedkernel.messaging.MessageID;
import com.mercedesbenz.sechub.sharedkernel.messaging.UserMessage;
import com.mercedesbenz.sechub.sharedkernel.usecases.admin.user.UseCaseAdminUpdatesUserEmailAddress;
import com.mercedesbenz.sechub.sharedkernel.validation.UserInputAssertion;

@Service
@RolesAllowed(RoleConstants.ROLE_SUPERADMIN)
public class UserEmailAddressUpdateService {

@Autowired
DomainMessageService eventBusService;

@Autowired
UserRepository userRepository;

@Autowired
AuditLogService auditLogService;

@Autowired
LogSanitizer logSanitizer;

@Autowired
UserInputAssertion assertion;

/* @formatter:off */
@Validated
@UseCaseAdminUpdatesUserEmailAddress(
@Step(
number = 2,
name = "Service updates user email address.",
next = { 3 },
description = "The service will update the user email address and also trigger events."))
/* @formatter:on */
@Transactional
public void updateUserEmailAddress(String userId, String newEmailAddress) {
assertion.isValidUserId(userId);
assertion.isValidEmailAddress(newEmailAddress);

User user = userRepository.findOrFailUser(userId);
String formerEmailAddress = user.getEmailAdress();

if (newEmailAddress.equalsIgnoreCase(formerEmailAddress)) {
throw new NotAcceptableException("User has already this email address");
}
/* parameters valid, we audit log the change */
auditLogService.log("Changed email adress of user {}", logSanitizer.sanitize(userId, 30));

user.emailAdress = newEmailAddress;

/* create message containing data before user email has changed */
UserMessage message = new UserMessage();
message.setUserId(user.getName());
message.setEmailAdress(user.getEmailAdress());
message.setFormerEmailAddress(formerEmailAddress);
message.setSubject("A SecHub administrator has changed your email address");

userRepository.save(user);

informUserEmailAddressUpdated(message);

}

@IsSendingAsyncMessage(MessageID.USER_EMAIL_ADDRESS_CHANGED)
private void informUserEmailAddressUpdated(UserMessage message) {

DomainMessage infoRequest = new DomainMessage(MessageID.USER_EMAIL_ADDRESS_CHANGED);
infoRequest.set(MessageDataKeys.USER_EMAIL_ADDRESS_CHANGE_DATA, message);

eventBusService.sendAsynchron(infoRequest);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ public class UserAdministrationRestControllerMockTest {
@MockBean
private UserRevokeSuperAdminRightsService userRevokeSuperAdminRightsService;

@MockBean
private UserEmailAddressUpdateService userEmailAddressUpdateService;

@MockBean
UserListService mockedUserListService;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// SPDX-License-Identifier: MIT
package com.mercedesbenz.sechub.domain.administration.user;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import com.mercedesbenz.sechub.sharedkernel.error.NotAcceptableException;
import com.mercedesbenz.sechub.sharedkernel.error.NotFoundException;
import com.mercedesbenz.sechub.sharedkernel.logging.AuditLogService;
import com.mercedesbenz.sechub.sharedkernel.logging.LogSanitizer;
import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessage;
import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessageService;
import com.mercedesbenz.sechub.sharedkernel.messaging.MessageDataKeys;
import com.mercedesbenz.sechub.sharedkernel.messaging.UserMessage;
import com.mercedesbenz.sechub.sharedkernel.validation.UserInputAssertion;
import com.mercedesbenz.sechub.test.TestCanaryException;

class UserEmailAddressUpdateServiceTest {

private static final String KNOWN_USER1 = "knownUser1";
private UserEmailAddressUpdateService serviceToTest;
private UserInputAssertion assertion;
private AuditLogService auditLogService;
private DomainMessageService eventBusService;
private UserRepository userRepository;
private LogSanitizer logSanitizer;

@BeforeEach
void beforeEach() {
serviceToTest = new UserEmailAddressUpdateService();

assertion = mock(UserInputAssertion.class);
auditLogService = mock(AuditLogService.class);
eventBusService = mock(DomainMessageService.class);
userRepository = mock(UserRepository.class);
logSanitizer = mock(LogSanitizer.class);

serviceToTest.assertion = assertion;
serviceToTest.auditLogService = auditLogService;
serviceToTest.logSanitizer = logSanitizer;
serviceToTest.eventBusService = eventBusService;
serviceToTest.userRepository = userRepository;
}

@Test
void audit_log_is_called_with_sanitized_user_id_when_user_was_found() {
/* prepare */
User knownUser1 = createKnownUser1();
when(userRepository.findOrFailUser(KNOWN_USER1)).thenReturn(knownUser1);
when(logSanitizer.sanitize(knownUser1.getName(), 30)).thenReturn("sanitized-userid");

/* execute */
serviceToTest.updateUserEmailAddress(KNOWN_USER1, "new.user1@example.com");

/* test */
verify(auditLogService).log(any(String.class), eq("sanitized-userid"));
}

@Test
void audit_log_is_NOT_called_when_user_not_found() {
/* prepare */
when(userRepository.findOrFailUser("notfound")).thenThrow(TestCanaryException.class);

/* execute */
assertThrows(TestCanaryException.class, () -> serviceToTest.updateUserEmailAddress("notfound", "new.user1@example.com"));

/* test */
verify(auditLogService, never()).log(any(String.class), any());
}

@Test
void asserts_email_address_parameter() {
/* prepare - just to have no NPE while testing with mock data */
User knownUser1 = createKnownUser1();
when(userRepository.findOrFailUser(KNOWN_USER1)).thenReturn(knownUser1);

/* execute */
serviceToTest.updateUserEmailAddress(KNOWN_USER1, "new.user1@example.com");

/* test */
verify(assertion).isValidEmailAddress("new.user1@example.com");
}

@Test
void asserts_user_id_parameter() {
/* prepare - just to have no NPE while testing with mock data */
User knownUser1 = createKnownUser1();
when(userRepository.findOrFailUser(KNOWN_USER1)).thenReturn(knownUser1);

/* execute */
serviceToTest.updateUserEmailAddress(KNOWN_USER1, "new.user1@example.com");

/* test */
verify(assertion).isValidUserId(KNOWN_USER1);
}

@Test
void asserts_user_id_parameter_before_user_is_fetched_from_db() {
/* prepare - just to have no NPE while testing with mock data */
when(userRepository.findOrFailUser("notfound")).thenThrow(NotFoundException.class);
doThrow(TestCanaryException.class).when(assertion).isValidUserId(any());

/* execute + test */
assertThrows(TestCanaryException.class, () -> serviceToTest.updateUserEmailAddress("novalid", "former.user1@example.com"));
}

@Test
void asserts_email_parameter_before_user_is_fetched_from_db() {
/* prepare - just to have no NPE while testing with mock data */
when(userRepository.findOrFailUser("notfound")).thenThrow(NotFoundException.class);
doThrow(TestCanaryException.class).when(assertion).isValidEmailAddress(any());

/* execute + test */
assertThrows(TestCanaryException.class, () -> serviceToTest.updateUserEmailAddress("notfound", "not-a-valid-email-adress"));
}

@Test
void when_assertions_do_not_handle_null_userid_user_repository_would_be_called_without_npe() {
/* prepare */
when(userRepository.findOrFailUser(null)).thenThrow(TestCanaryException.class);

/* execute */
assertThrows(TestCanaryException.class, () -> serviceToTest.updateUserEmailAddress(null, "something"));
}

@Test
void when_assertions_do_not_handle_null_email_user_repository_would_be_called_without_npe() {
/* prepare */
when(userRepository.findOrFailUser("somebody")).thenThrow(TestCanaryException.class);

/* execute */
assertThrows(TestCanaryException.class, () -> serviceToTest.updateUserEmailAddress("somebody", null));
}

@Test
void throws_not_acceptable_when_same_mail_adress_as_before() {
/* prepare */
User knownUser1 = createKnownUser1();
when(userRepository.findOrFailUser(KNOWN_USER1)).thenReturn(knownUser1);

/* execute + test (exception ) */
assertThrows(NotAcceptableException.class, () -> serviceToTest.updateUserEmailAddress(KNOWN_USER1, "former.user1@example.com"));
}

@Test
void throws_exception_when_user_not_found() {
/* prepare */
when(userRepository.findOrFailUser(any())).thenThrow(NotFoundException.class);

/* execute + test (exception ) */
assertThrows(NotFoundException.class, () -> serviceToTest.updateUserEmailAddress("notfound", "new.user1@example.com"));
}

@Test
void saves_user_when_parameters_are_valid() {
/* prepare */
User knownUser1 = createKnownUser1();
when(userRepository.findOrFailUser(KNOWN_USER1)).thenReturn(knownUser1);

/* execute */
serviceToTest.updateUserEmailAddress(KNOWN_USER1, "new.user1@example.com");

/* test */
// check the user object has new mail address when saved:
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
verify(userRepository).save(userCaptor.capture());
assertEquals("new.user1@example.com", userCaptor.getValue().getEmailAdress());
}

@Test
void sends_event_with_user_data_when_parameters_are_valid() {
/* prepare */
User knownUser1 = createKnownUser1();
when(userRepository.findOrFailUser(KNOWN_USER1)).thenReturn(knownUser1);

/* execute */
serviceToTest.updateUserEmailAddress(KNOWN_USER1, "new.user1@example.com");

/* test */
// check event is sent with expected data
ArgumentCaptor<DomainMessage> messageCaptor = ArgumentCaptor.forClass(DomainMessage.class);
verify(eventBusService).sendAsynchron(messageCaptor.capture());
UserMessage userMessage = messageCaptor.getValue().get(MessageDataKeys.USER_EMAIL_ADDRESS_CHANGE_DATA);
assertNotNull(userMessage);

assertEquals(KNOWN_USER1, userMessage.getUserId());
assertEquals("new.user1@example.com", userMessage.getEmailAdress());
assertEquals("former.user1@example.com", userMessage.getFormerEmailAddress());
}

private User createKnownUser1() {
User user = new User();
user.name = KNOWN_USER1;
user.emailAdress = "former.user1@example.com";
return user;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,11 @@ public String deleteUser(String userId) {
return "sent";
}

public String changeUserEmailAddress(String userId, String newEmailAddress) {
asTestUser().changeEmailAddress(userId, newEmailAddress);
return "sent";
}

public String cancelJob(UUID jobUUID) {
getRestHelper().post(getUrlBuilder().buildAdminCancelsJob(jobUUID));
return "cancel triggered";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
import com.mercedesbenz.sechub.developertools.admin.ui.action.user.ShowAdminListAction;
import com.mercedesbenz.sechub.developertools.admin.ui.action.user.ShowUserDetailAction;
import com.mercedesbenz.sechub.developertools.admin.ui.action.user.ShowUserListAction;
import com.mercedesbenz.sechub.developertools.admin.ui.action.user.UpdateUserEmailAction;
import com.mercedesbenz.sechub.developertools.admin.ui.action.user.privileges.GrantAdminRightsToUserAction;
import com.mercedesbenz.sechub.developertools.admin.ui.action.user.privileges.RevokeAdminRightsFromAdminAction;
import com.mercedesbenz.sechub.domain.scan.product.ProductIdentifier;
Expand Down Expand Up @@ -291,6 +292,7 @@ private void createUserMenu() {
add(menu, new DeleteUserAction(context));
menu.addSeparator();
add(menu, new ShowUserDetailAction(context));
add(menu, new UpdateUserEmailAction(context));
add(menu, new AnonymousRequestNewAPITokenUserAction(context));
menu.addSeparator();
add(menu, new ListSignupsAction(context));
Expand Down
Loading