diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/account/EmailAccountCreationService.java b/server/src/main/java/org/cloudfoundry/identity/uaa/account/EmailAccountCreationService.java index f96870efaa9..5cac18629c3 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/account/EmailAccountCreationService.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/account/EmailAccountCreationService.java @@ -1,38 +1,45 @@ package org.cloudfoundry.identity.uaa.account; import com.fasterxml.jackson.core.type.TypeReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.error.UaaException; import org.cloudfoundry.identity.uaa.message.MessageService; import org.cloudfoundry.identity.uaa.message.MessageType; +import org.cloudfoundry.identity.uaa.oauth.provider.ClientDetails; +import org.cloudfoundry.identity.uaa.provider.NoSuchClientException; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceAlreadyExistsException; import org.cloudfoundry.identity.uaa.scim.util.ScimUtils; import org.cloudfoundry.identity.uaa.scim.validate.PasswordValidator; import org.cloudfoundry.identity.uaa.util.JsonUtils; -import org.cloudfoundry.identity.uaa.provider.NoSuchClientException; -import org.cloudfoundry.identity.uaa.zone.MultitenantClientServices; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.MergedZoneBrandingInformation; +import org.cloudfoundry.identity.uaa.zone.MultitenantClientServices; import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; -import org.cloudfoundry.identity.uaa.oauth.provider.ClientDetails; +import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.client.HttpClientErrorException; import org.thymeleaf.context.Context; import org.thymeleaf.spring5.SpringTemplateEngine; -import java.util.*; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import static org.cloudfoundry.identity.uaa.codestore.ExpiringCodeType.REGISTRATION; import static org.cloudfoundry.identity.uaa.util.UaaUrlUtils.findMatchingRedirectUri; import static org.springframework.http.HttpStatus.BAD_REQUEST; +@Service public class EmailAccountCreationService implements AccountCreationService { private final Logger logger = LoggerFactory.getLogger(getClass()); @@ -48,7 +55,7 @@ public class EmailAccountCreationService implements AccountCreationService { private final IdentityZoneManager identityZoneManager; public EmailAccountCreationService( - SpringTemplateEngine templateEngine, + @Qualifier("mailTemplateEngine") SpringTemplateEngine templateEngine, MessageService messageService, ExpiringCodeStore codeStore, ScimUserProvisioning scimUserProvisioning, diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/account/EmailChangeEmailService.java b/server/src/main/java/org/cloudfoundry/identity/uaa/account/EmailChangeEmailService.java index 44e84b6e0e2..92606560c19 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/account/EmailChangeEmailService.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/account/EmailChangeEmailService.java @@ -7,25 +7,32 @@ import org.cloudfoundry.identity.uaa.error.UaaException; import org.cloudfoundry.identity.uaa.message.MessageService; import org.cloudfoundry.identity.uaa.message.MessageType; +import org.cloudfoundry.identity.uaa.oauth.provider.ClientDetails; +import org.cloudfoundry.identity.uaa.provider.NoSuchClientException; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.UaaUrlUtils; -import org.cloudfoundry.identity.uaa.provider.NoSuchClientException; import org.cloudfoundry.identity.uaa.zone.MergedZoneBrandingInformation; import org.cloudfoundry.identity.uaa.zone.MultitenantClientServices; import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager; -import org.cloudfoundry.identity.uaa.oauth.provider.ClientDetails; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; import java.sql.Timestamp; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import static org.cloudfoundry.identity.uaa.codestore.ExpiringCodeType.EMAIL; import static org.cloudfoundry.identity.uaa.util.UaaUrlUtils.findMatchingRedirectUri; +@Service public class EmailChangeEmailService implements ChangeEmailService { static final String CHANGE_EMAIL_REDIRECT_URL = "change_email_redirect_url"; @@ -38,7 +45,8 @@ public class EmailChangeEmailService implements ChangeEmailService { private final MultitenantClientServices clientDetailsService; private final IdentityZoneManager identityZoneManager; - EmailChangeEmailService(final TemplateEngine templateEngine, + EmailChangeEmailService( + @Qualifier("mailTemplateEngine") final TemplateEngine templateEngine, final MessageService messageService, final ScimUserProvisioning scimUserProvisioning, final ExpiringCodeStore codeStore, diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/account/UaaChangePasswordService.java b/server/src/main/java/org/cloudfoundry/identity/uaa/account/UaaChangePasswordService.java index 14eceac726a..5361a09ef33 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/account/UaaChangePasswordService.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/account/UaaChangePasswordService.java @@ -28,6 +28,7 @@ import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; import java.util.Date; import java.util.List; @@ -35,6 +36,7 @@ import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; +@Service public class UaaChangePasswordService implements ChangePasswordService, ApplicationEventPublisherAware { private final ScimUserProvisioning scimUserProvisioning; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/account/UaaResetPasswordService.java b/server/src/main/java/org/cloudfoundry/identity/uaa/account/UaaResetPasswordService.java index fcb60579871..312d3ab0506 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/account/UaaResetPasswordService.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/account/UaaResetPasswordService.java @@ -27,6 +27,7 @@ import org.springframework.core.io.support.ResourcePropertySource; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; import org.cloudfoundry.identity.uaa.oauth.provider.ClientDetails; import java.sql.Timestamp; @@ -39,6 +40,7 @@ import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; import static org.springframework.util.StringUtils.isEmpty; +@Service("resetPasswordService") public class UaaResetPasswordService implements ResetPasswordService, ApplicationEventPublisherAware { public static final int PASSWORD_RESET_LIFETIME = 30 * 60 * 1000; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/LoginServerConfig.java b/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/LoginServerConfig.java index d681f32fff7..83bea41f9b3 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/LoginServerConfig.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/LoginServerConfig.java @@ -1,21 +1,138 @@ package org.cloudfoundry.identity.uaa.impl.config; import org.cloudfoundry.identity.uaa.message.EmailService; +import org.cloudfoundry.identity.uaa.message.LocalUaaRestTemplate; import org.cloudfoundry.identity.uaa.message.MessageService; +import org.cloudfoundry.identity.uaa.message.MessageType; import org.cloudfoundry.identity.uaa.message.NotificationsService; +import org.cloudfoundry.identity.uaa.message.util.FakeJavaMailSender; +import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.Environment; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Primary; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * Configuration for sending e-mails or HTTP-based notifications, e.g. on account creation. + *

+ * If a {@code notifications.url} property is defined, it uses HTTP-based notifications. + * Otherwise, if {@code smtp.*} properties are defined, it will send e-mails. If none of + * these properties are configured, it uses a {@link FakeJavaMailSender} mailer. + *

+ * All beans are marked lazy except {@link NotificationConfiguration#notificationMessageService}, + * as they are all "fallback" options and do not need to be created when {@code notifications.url} + * is defined. + */ +@Lazy @Configuration +@EnableConfigurationProperties(SmtpProperties.class) public class LoginServerConfig { + /** + * Fallback bean for when there is no "notifications.url". + * TODO: dgarnier annotate with @Fallback in Boot 3.4 + * + * @return - + */ @Bean - public MessageService messageService(EmailService emailService, NotificationsService notificationsService, Environment environment) { - if (environment.getProperty("notifications.url") != null && !"".equals(environment.getProperty("notifications.url"))) { - return notificationsService; - } else { - return emailService; + public MessageService emailMessageService( + // dgarnier: use DEFAULT_UAA_URL + @Value("${login.url:http://localhost:8080/uaa}") String loginUrl, + JavaMailSender mailSender, + SmtpProperties smtpProperties, + IdentityZoneManager identityZoneManager) { + return new EmailService( + mailSender, + loginUrl, + smtpProperties.fromAddress(), + identityZoneManager + ); + } + + /** + * Fallback for SMTP mail sender, when no real mail sender is used. This is mostly used in tests. + * TODO: dgarnier annotate with @Fallback in Boot 3.4 + * + * @return - + */ + @Bean + JavaMailSender fakeJavaMailSender() { + return new FakeJavaMailSender(); + } + + @Bean + @Primary + @ConditionalOnProperty(value = "smtp.host", matchIfMissing = false) + JavaMailSender smtpMailSender(SmtpProperties smtpProperties) { + var mailSender = new JavaMailSenderImpl(); + mailSender.setHost(smtpProperties.host()); + mailSender.setPort(smtpProperties.port()); + mailSender.setPassword(smtpProperties.password()); + mailSender.setUsername(smtpProperties.user()); + + var javaMailProperties = new Properties(); + javaMailProperties.put("mail.smtp.auth", smtpProperties.auth()); + javaMailProperties.put("mail.smtp.starttls.enable", smtpProperties.starttls()); + javaMailProperties.put("mail.smtp.ssl.protocols", smtpProperties.sslprotocols()); + mailSender.setJavaMailProperties(javaMailProperties); + return mailSender; + } + + @Configuration + @ConditionalOnProperty(value = "notifications.url", matchIfMissing = false) + @EnableConfigurationProperties(NotificationsProperties.class) + static class NotificationConfiguration { + + /** + * HTTP-based {@link MessageService}. Takes precedence over any email-basedO + * configuration. + * + * @param notificationsTemplate - + * @param notificationsProperties - + * @return - + */ + @Bean + @Primary + public MessageService notificationMessageService( + LocalUaaRestTemplate notificationsTemplate, + NotificationsProperties notificationsProperties + ) { + return new NotificationsService( + notificationsTemplate, + notificationsProperties.url(), + notifications(), + notificationsProperties.sendInDefaultZone() + ); + } + + private static Map> notifications() { + return Map.of( + MessageType.CREATE_ACCOUNT_CONFIRMATION, notification("Send activation code", "f7a85fdc-d920-41f0-b3a4-55db08e408ce"), + MessageType.PASSWORD_RESET, notification("Reset Password", "141200f6-93bd-4761-a721-941ab511ba2c"), + MessageType.CHANGE_EMAIL, notification("Change Email", "712de257-a7fa-44cb-b1ac-8a6588d1be23"), + MessageType.INVITATION, notification("Invitation", "e6722687-3f0f-4e7a-9925-839a04712cea") + ); } + + private static HashMap notification(String description, String id) { + return new HashMap<>( + Map.of( + "description", description, + "id", id, + "critical", true + ) + ); + } + } + } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/NotificationsProperties.java b/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/NotificationsProperties.java new file mode 100644 index 00000000000..7bf534eef45 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/NotificationsProperties.java @@ -0,0 +1,11 @@ +package org.cloudfoundry.identity.uaa.impl.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.DefaultValue; + +@ConfigurationProperties(prefix = "notifications") +record NotificationsProperties( + String url, + @DefaultValue("true") boolean sendInDefaultZone +) { +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/SmtpProperties.java b/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/SmtpProperties.java new file mode 100644 index 00000000000..a3cd5f35ab6 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/SmtpProperties.java @@ -0,0 +1,17 @@ +package org.cloudfoundry.identity.uaa.impl.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.DefaultValue; + +@ConfigurationProperties(prefix = "smtp") +public record SmtpProperties( + @DefaultValue("localhost") String host, + @DefaultValue("25") int port, + @DefaultValue("") String user, + @DefaultValue("") String password, + @DefaultValue("false") boolean auth, + @DefaultValue("false") boolean starttls, + @DefaultValue("TLSv1.2") String sslprotocols, + @DefaultValue("") String fromAddress +) { +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/login/ForcePasswordChangeController.java b/server/src/main/java/org/cloudfoundry/identity/uaa/login/ForcePasswordChangeController.java index fc7d1348bac..7ef6f81e19f 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/login/ForcePasswordChangeController.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/login/ForcePasswordChangeController.java @@ -35,7 +35,7 @@ public class ForcePasswordChangeController { public ForcePasswordChangeController( final ResourcePropertySource resourcePropertySource, - final @Qualifier("resetPasswordService") ResetPasswordService resetPasswordService) { + ResetPasswordService resetPasswordService) { this.resourcePropertySource = resourcePropertySource; this.resetPasswordService = resetPasswordService; } diff --git a/server/src/main/resources/spring/login-ui.xml b/server/src/main/resources/spring/login-ui.xml index 5f6683630f8..a913e530bae 100644 --- a/server/src/main/resources/spring/login-ui.xml +++ b/server/src/main/resources/spring/login-ui.xml @@ -310,37 +310,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -395,74 +364,14 @@ - - - - - - - - - - - - - - ${smtp.auth:false} - ${smtp.starttls:false} - ${smtp.sslprotocols:TLSv1.2} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/AccountsControllerMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/AccountsControllerMockMvcTests.java index af230ad381f..385dc897f86 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/AccountsControllerMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/AccountsControllerMockMvcTests.java @@ -1,5 +1,7 @@ package org.cloudfoundry.identity.uaa.login; +import java.util.Collections; + import org.apache.commons.lang3.RandomStringUtils; import org.cloudfoundry.identity.uaa.DefaultTestContext; import org.cloudfoundry.identity.uaa.account.EmailAccountCreationService; @@ -29,6 +31,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mock.env.MockPropertySource; @@ -42,9 +45,6 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.StandardServletEnvironment; - -import java.util.Collections; - import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; @@ -85,17 +85,17 @@ class AccountsControllerMockMvcTests { @Autowired private FakeJavaMailSender fakeJavaMailSender; private JavaMailSender originalEmailSender; + @Autowired + private EmailService emailService; @BeforeEach void setUp() { testClient = new TestClient(mockMvc); - EmailService emailService = webApplicationContext.getBean("emailService", EmailService.class); originalEmailSender = emailService.getMailSender(); emailService.setMailSender(fakeJavaMailSender); userEmail = "user" + new AlphanumericRandomValueStringGenerator().generate() + "@example.com"; - assertNotNull(webApplicationContext.getBean("messageService")); IdentityZoneHolder.setProvisioning(webApplicationContext.getBean(IdentityZoneProvisioning.class)); mockMvcTestClient = new MockMvcTestClient(mockMvc); @@ -111,7 +111,7 @@ private void setProperty(String name, String value) { @AfterEach void clearEmails() { - webApplicationContext.getBean("emailService", EmailService.class).setMailSender(originalEmailSender); + emailService.setMailSender(originalEmailSender); fakeJavaMailSender.clearMessage(); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java index 6b38d68b7a8..eeabd124825 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java @@ -78,7 +78,6 @@ public class InvitationsServiceMockMvcTests { @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") @Autowired - @Qualifier("emailService") EmailService emailService; public static final String REDIRECT_URI = "http://invitation.redirect.test";