Merge branch 'RED-6903' into 'main'

Red-6903 - Implement email service in tenant user management service

See merge request fforesight/tenant-user-management-service!20
This commit is contained in:
Ali Oezyetimoglu 2023-09-05 15:03:28 +02:00
commit 9d9aa6f46d
7 changed files with 287 additions and 34 deletions

View File

@ -100,6 +100,8 @@ dependencies {
implementation("commons-validator:commons-validator:1.7")
implementation("org.springframework.boot:spring-boot-configuration-processor")
implementation("com.iqser.red.commons:storage-commons:2.43.0")
implementation("jakarta.mail:jakarta.mail-api:2.1.2")
implementation("org.eclipse.angus:angus-mail:2.0.2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.cloud:spring-cloud-starter-openfeign")
testImplementation("org.projectlombok:lombok")
@ -111,8 +113,8 @@ dependencies {
testImplementation("org.springframework.amqp:spring-rabbit-test")
testImplementation("org.testcontainers:postgresql:1.18.3")
testImplementation("com.github.dasniko:testcontainers-keycloak:2.5.0")
testImplementation("org.testcontainers:testcontainers:1.18.3")
testImplementation("org.testcontainers:junit-jupiter:1.18.3")
testImplementation("org.testcontainers:testcontainers:1.19.0")
testImplementation("org.testcontainers:junit-jupiter:1.19.0")
testAnnotationProcessor("org.projectlombok:lombok")
}

View File

@ -6,11 +6,12 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import com.knecon.fforesight.tenantusermanagement.model.SMTPConfiguration;
import com.knecon.fforesight.tenantusermanagement.model.SMTPResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@ -22,7 +23,6 @@ public interface SMTPConfigurationResource {
String TEST_PATH = "/test";
@ResponseBody
@ResponseStatus(value = HttpStatus.OK)
@GetMapping(value = SMTP_PATH, produces = MediaType.APPLICATION_JSON_VALUE)
@ -38,11 +38,12 @@ public interface SMTPConfigurationResource {
void updateSMTPConfiguration(@RequestBody SMTPConfiguration smtpConfigurationModel);
@ResponseBody
@ResponseStatus(value = HttpStatus.OK)
@PostMapping(value = SMTP_PATH + TEST_PATH, consumes = MediaType.APPLICATION_JSON_VALUE)
@PostMapping(value = SMTP_PATH + TEST_PATH, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Test SMTP Settings to KeyCloak")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "SMTP Configuration is valid."), @ApiResponse(responseCode = "400", description = "SMTP test failed.")})
void testSMTPConfiguration(@RequestBody SMTPConfiguration smtpConfigurationModel);
SMTPResponse testSMTPConfiguration(@RequestBody SMTPConfiguration smtpConfigurationModel);
@ResponseStatus(value = HttpStatus.NO_CONTENT)

View File

@ -6,8 +6,10 @@ import static com.knecon.fforesight.tenantusermanagement.permissions.UserManagem
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -16,6 +18,8 @@ import com.knecon.fforesight.tenantcommons.TenantContext;
import com.knecon.fforesight.tenantusermanagement.api.external.PublicResource;
import com.knecon.fforesight.tenantusermanagement.api.external.SMTPConfigurationResource;
import com.knecon.fforesight.tenantusermanagement.model.SMTPConfiguration;
import com.knecon.fforesight.tenantusermanagement.model.SMTPResponse;
import com.knecon.fforesight.tenantusermanagement.service.EmailService;
import com.knecon.fforesight.tenantusermanagement.service.RealmService;
import lombok.RequiredArgsConstructor;
@ -28,9 +32,11 @@ import lombok.extern.slf4j.Slf4j;
public class SMTPConfigurationController implements SMTPConfigurationResource, PublicResource {
private final static String SMTP_PASSWORD_KEY = "FFORESIGHT_SMTP_PASSWORD";
private final static String DEFAULT_PASSWORD = "**********";
private final RealmService realmService;
private final ObjectMapper objectMapper;
private final EncryptionDecryptionService encryptionDecryptionService;
private final EmailService emailService;
@Override
@ -58,6 +64,36 @@ public class SMTPConfigurationController implements SMTPConfigurationResource, P
realmService.realm(TenantContext.getTenantId()).update(realmRepresentation);
}
@SneakyThrows
@Override
@PreAuthorize("hasAuthority('" + WRITE_SMTP_CONFIGURATION + "')")
public SMTPResponse testSMTPConfiguration(@RequestBody SMTPConfiguration smtpConfiguration) {
String targetEmail = realmService.getEmail(realmService.realm(TenantContext.getTenantId()));
if (StringUtils.isEmpty(targetEmail)) {
// will send e-mail to self in case targetEmail is not set
targetEmail = smtpConfiguration.getFrom();
}
updatePassword(smtpConfiguration);
smtpConfiguration.setPassword(encryptionDecryptionService.decrypt(smtpConfiguration.getPassword()));
SMTPResponse smtpResponse = emailService.send(convertSMTPConfigurationModelToMap(smtpConfiguration), targetEmail, "Redaction Test message", "This is a test message");
log.info("Test SMTP Configuration status: {}, reason: {}", smtpResponse.getStatusCode(), smtpResponse.getReasonPhrase());
return smtpResponse;
}
@Override
@PreAuthorize("hasAuthority('" + WRITE_SMTP_CONFIGURATION + "')")
public void clearSMTPConfiguration() {
// also update in KC
var realmRepresentation = realmService.realm(TenantContext.getTenantId()).toRepresentation();
realmRepresentation.setSmtpServer(new HashMap<>());
realmRepresentation.getAttributesOrEmpty().remove(SMTP_PASSWORD_KEY);
realmService.realm(TenantContext.getTenantId()).update(realmRepresentation);
}
private Map<String, String> convertSMTPConfigurationModelToMap(SMTPConfiguration smtpConfigurationModel) {
@ -73,26 +109,19 @@ public class SMTPConfigurationController implements SMTPConfigurationResource, P
return stringPropertiesMap;
}
private void updatePassword(SMTPConfiguration smtpConfiguration) {
@SneakyThrows
@Override
@PreAuthorize("hasAuthority('" + WRITE_SMTP_CONFIGURATION + "')")
public void testSMTPConfiguration(@RequestBody SMTPConfiguration smtpConfigurationModel) {
var propertiesMap = convertSMTPConfigurationModelToMap(smtpConfigurationModel);
realmService.realm(TenantContext.getTenantId()).testSMTPConnection(propertiesMap);
if (DEFAULT_PASSWORD.equals(smtpConfiguration.getPassword())) {
try {
var password = realmService.realm(TenantContext.getTenantId()).toRepresentation().getAttributesOrEmpty().get(SMTP_PASSWORD_KEY);
smtpConfiguration.setPassword(password);
} catch (Exception e) {
log.info("No current SMTP Config exists", e);
}
} else {
smtpConfiguration.setPassword(encryptionDecryptionService.encrypt(smtpConfiguration.getPassword()));
}
}
@Override
@PreAuthorize("hasAuthority('" + WRITE_SMTP_CONFIGURATION + "')")
public void clearSMTPConfiguration() {
// also update in KC
var realmRepresentation = realmService.realm(TenantContext.getTenantId()).toRepresentation();
realmRepresentation.setSmtpServer(new HashMap<>());
realmRepresentation.getAttributesOrEmpty().remove(SMTP_PASSWORD_KEY);
realmService.realm(TenantContext.getTenantId()).update(realmRepresentation);
}
}

View File

@ -0,0 +1,22 @@
package com.knecon.fforesight.tenantusermanagement.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "Object containing a simplified version of the SMTP test connection response.")
public class SMTPResponse {
@Schema(description = "Parameter containing status code of the response.")
private int statusCode;
@Schema(description = "Parameter containing the reason phrase of the response.")
private String reasonPhrase;
}

View File

@ -0,0 +1,169 @@
package com.knecon.fforesight.tenantusermanagement.service;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
import java.util.Properties;
import javax.net.ssl.SSLContext;
import javax.ws.rs.BadRequestException;
import org.springframework.stereotype.Service;
import com.knecon.fforesight.tenantusermanagement.model.SMTPResponse;
import jakarta.mail.Address;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.Multipart;
import jakarta.mail.Session;
import jakarta.mail.Transport;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeBodyPart;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import jakarta.mail.internet.MimeUtility;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class EmailService {
public SMTPResponse send(Map<String, String> config, String address, String subject, String textBody) {
Transport transport = null;
try {
Properties props = new Properties();
if (config.containsKey("host")) {
props.setProperty("mail.smtp.host", config.get("host"));
}
boolean auth = "true".equals(config.get("auth"));
boolean ssl = "true".equals(config.get("ssl"));
boolean starttls = "true".equals(config.get("starttls"));
if (config.containsKey("port") && config.get("port") != null) {
props.setProperty("mail.smtp.port", config.get("port"));
}
if (auth) {
props.setProperty("mail.smtp.auth", "true");
}
if (ssl) {
props.setProperty("mail.smtp.ssl.enable", "true");
}
if (starttls) {
props.setProperty("mail.smtp.starttls.enable", "true");
}
if (ssl || starttls) {
props.put("mail.smtp.ssl.protocols", getSupportedSslProtocols());
}
props.setProperty("mail.smtp.timeout", "10000");
props.setProperty("mail.smtp.connectiontimeout", "10000");
String from = config.get("from");
String fromDisplayName = config.get("fromDisplayName");
String replyTo = config.get("replyTo");
String replyToDisplayName = config.get("replyToDisplayName");
String envelopeFrom = config.get("envelopeFrom");
Session session = Session.getInstance(props);
Multipart multipart = new MimeMultipart("alternative");
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText(textBody, "UTF-8");
multipart.addBodyPart(textPart);
Message msg = new MimeMessage(session);
msg.setFrom(toInternetAddress(from, fromDisplayName));
msg.setReplyTo(new Address[]{toInternetAddress(from, fromDisplayName)});
if (isNotBlank(replyTo)) {
msg.setReplyTo(new Address[]{toInternetAddress(replyTo, replyToDisplayName)});
}
if (isNotBlank(envelopeFrom)) {
props.setProperty("mail.smtp.from", envelopeFrom);
}
msg.setHeader("To", address);
msg.setSubject(MimeUtility.encodeText(subject, StandardCharsets.UTF_8.name(), null));
msg.setContent(multipart);
msg.saveChanges();
msg.setSentDate(new Date());
transport = session.getTransport("smtp");
if (auth) {
transport.connect(config.get("user"), config.get("password"));
} else {
transport.connect();
}
transport.sendMessage(msg, new InternetAddress[]{new InternetAddress(address)});
return SMTPResponse.builder()
.statusCode(200)
.build();
} catch (Exception e) {
return SMTPResponse.builder()
.statusCode(400)
.reasonPhrase(e.getMessage())
.build();
} finally {
if (transport != null) {
try {
transport.close();
} catch (MessagingException e) {
log.warn("Failed to close transport", e);
}
}
}
}
protected InternetAddress toInternetAddress(String email, String displayName) throws UnsupportedEncodingException, AddressException, BadRequestException {
if (email == null || "".equals(email.trim())) {
throw new BadRequestException("Please provide a valid address");
}
if (displayName == null || "".equals(displayName.trim())) {
return new InternetAddress(email);
}
return new InternetAddress(email, displayName, "utf-8");
}
private String getSupportedSslProtocols() {
try {
String[] protocols = SSLContext.getDefault().getSupportedSSLParameters().getProtocols();
if (protocols != null) {
return String.join(" ", protocols);
}
} catch (Exception e) {
log.warn("Failed to get list of supported SSL protocols", e);
}
return null;
}
public boolean isNotBlank(String str) {
return str != null && !"".equals(str.trim());
}
}

View File

@ -1,9 +1,11 @@
package com.knecon.fforesight.tenantusermanagement.service;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.account.UserRepresentation;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.knecon.fforesight.keycloakcommons.security.KeycloakSecurity;
import com.knecon.fforesight.tenantcommons.model.AuthDetails;
import com.knecon.fforesight.tenantusermanagement.properties.TenantUserManagementProperties;
@ -25,6 +27,11 @@ public class RealmService {
return keycloak.getAdminClient().realm(tenantId);
}
public String getEmail(RealmResource resource) {
var user = resource.users().list().stream().filter(userRepresentation -> userRepresentation.getUsername().equals("admin")).findFirst();
return user.isPresent() ? user.get().getEmail() : "";
}
public AuthDetails getOpenIdConnectDetails(String tenantId) {

View File

@ -22,20 +22,12 @@ public class SMTPConfigurationTest extends AbstractTenantUserManagementIntegrati
TenantContext.setTenantId(AbstractTenantUserManagementIntegrationTest.TEST_TENANT_ID);
try {
var currentSMTPConfiguration = smtpConfigurationClient.getCurrentSMTPConfiguration();
smtpConfigurationClient.getCurrentSMTPConfiguration();
} catch (Exception e) {
System.out.println(e.getMessage());
}
SMTPConfiguration newConfig = new SMTPConfiguration();
newConfig.setAuth(true);
newConfig.setFrom("from@knecon.com");
newConfig.setHost("test.knecon.com");
newConfig.setPassword("secret");
newConfig.setUser("user");
newConfig.setStarttls(true);
newConfig.setSsl(false);
smtpConfigurationClient.updateSMTPConfiguration(newConfig);
smtpConfigurationClient.updateSMTPConfiguration(provideTestSMTPConfiguration());
var currentSMTPConfiguration = smtpConfigurationClient.getCurrentSMTPConfiguration();
assertThat(currentSMTPConfiguration.getPassword()).matches("\\**");
@ -50,4 +42,35 @@ public class SMTPConfigurationTest extends AbstractTenantUserManagementIntegrati
}
@Test
public void testSMTPConnection() {
TenantContext.setTenantId(AbstractTenantUserManagementIntegrationTest.TEST_TENANT_ID);
SMTPConfiguration smtpConfiguration = provideTestSMTPConfiguration();
var response = smtpConfigurationClient.testSMTPConfiguration(smtpConfiguration);
// Fails because we are not using a smtp config
assertThat(response.getStatusCode()).isEqualTo(400);
assertThat(response.getReasonPhrase()).isEqualTo("Couldn't connect to host, port: test.knecon.com, 25; timeout 10000");
TenantContext.clear();
}
private SMTPConfiguration provideTestSMTPConfiguration() {
SMTPConfiguration smtpConfiguration = new SMTPConfiguration();
smtpConfiguration.setAuth(true);
smtpConfiguration.setFrom("from@knecon.com");
smtpConfiguration.setHost("test.knecon.com");
smtpConfiguration.setPassword("secret");
smtpConfiguration.setUser("user");
smtpConfiguration.setStarttls(true);
smtpConfiguration.setSsl(false);
return smtpConfiguration;
}
}