Merge pull request #4259 from stianst/abstractj-KEYCLOAK-4444

KEYCLOAK-4444
This commit is contained in:
Stian Thorgersen 2017-06-27 10:44:30 +02:00 committed by GitHub
commit 56c5996aff
14 changed files with 293 additions and 21 deletions

View file

@ -38,6 +38,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@ -184,6 +185,12 @@ public interface RealmResource {
@QueryParam("bindDn") String bindDn, @QueryParam("bindCredential") String bindCredential,
@QueryParam("useTruststoreSpi") String useTruststoreSpi, @QueryParam("connectionTimeout") String connectionTimeout);
@Path("testSMTPConnection/{config}")
@POST
@NoCache
@Consumes(MediaType.APPLICATION_JSON)
Response testSMTPConnection(final @PathParam("config") String config) throws Exception;
@Path("clear-realm-cache")
@POST
void clearRealmCache();

View file

@ -17,15 +17,15 @@
package org.keycloak.email;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface EmailSenderProvider extends Provider {
void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException;
void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException;
}

View file

@ -22,6 +22,8 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -46,6 +48,15 @@ public interface EmailTemplateProvider extends Provider {
*/
public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException;
/**
* Test SMTP connection with current logged in user
*
* @param config SMTP server configuration
* @param user SMTP recipient
* @throws EmailException
*/
public void sendSmtpTestEmail(Map<String, String> config, UserModel user) throws EmailException;
/**
* Send to confirm that user wants to link his account with identity broker link
*/

View file

@ -20,7 +20,6 @@ package org.keycloak.email;
import com.sun.mail.smtp.SMTPMessage;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger;
import org.keycloak.truststore.HostnameVerificationPolicy;
@ -57,20 +56,22 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
}
@Override
public void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
public void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
Transport transport = null;
try {
String address = retrieveEmailAddress(user);
Map<String, String> config = realm.getSmtpConfig();
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")) {
if (config.containsKey("port") && config.get("port") != null) {
props.setProperty("mail.smtp.port", config.get("port"));
}
@ -153,7 +154,10 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
}
}
protected InternetAddress toInternetAddress(String email, String displayName) throws UnsupportedEncodingException, AddressException {
protected InternetAddress toInternetAddress(String email, String displayName) throws UnsupportedEncodingException, AddressException, EmailException {
if (email == null || "".equals(email.trim())) {
throw new EmailException("Please provide a valid address", null);
}
if (displayName == null || "".equals(displayName.trim())) {
return new InternetAddress(email);
}

View file

@ -107,6 +107,19 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
send("passwordResetSubject", "password-reset.ftl", attributes);
}
@Override
public void sendSmtpTestEmail(Map<String, String> config, UserModel user) throws EmailException {
setRealm(session.getContext().getRealm());
setUser(user);
Map<String, Object> attributes = new HashMap<String, Object>();
attributes.put("user", new ProfileBean(user));
attributes.put("realmName", realm.getName());
EmailTemplate email = processTemplate("emailTestSubject", Collections.emptyList(), "email-test.ftl", attributes);
send(config, email.getSubject(), email.getTextBody(), email.getHtmlBody());
}
@Override
public void sendConfirmIdentityBrokerLink(String link, long expirationInMinutes) throws EmailException {
Map<String, Object> attributes = new HashMap<String, Object>();
@ -156,7 +169,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
send(subjectKey, Collections.emptyList(), template, attributes);
}
private void send(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
private EmailTemplate processTemplate(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
try {
ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
Theme theme = themeProvider.getTheme(realm.getEmailTheme(), Theme.Type.EMAIL);
@ -180,15 +193,27 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
htmlBody = null;
}
send(subject, textBody, htmlBody);
return new EmailTemplate(subject, textBody, htmlBody);
} catch (Exception e) {
throw new EmailException("Failed to template email", e);
}
}
private void send(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
try {
EmailTemplate email = processTemplate(subjectKey, subjectAttributes, template, attributes);
send(email.getSubject(), email.getTextBody(), email.getHtmlBody());
} catch (Exception e) {
throw new EmailException("Failed to template email", e);
}
}
private void send(String subject, String textBody, String htmlBody) throws EmailException {
send(realm.getSmtpConfig(), subject, textBody, htmlBody);
}
private void send(Map<String, String> config, String subject, String textBody, String htmlBody) throws EmailException {
EmailSenderProvider emailSender = session.getProvider(EmailSenderProvider.class);
emailSender.send(realm, user, subject, textBody, htmlBody);
emailSender.send(config, user, subject, textBody, htmlBody);
}
@Override
@ -203,4 +228,29 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
return sb.toString();
}
private class EmailTemplate {
private String subject;
private String textBody;
private String htmlBody;
public EmailTemplate(String subject, String textBody, String htmlBody) {
this.subject = subject;
this.textBody = textBody;
this.htmlBody = htmlBody;
}
public String getSubject() {
return subject;
}
public String getTextBody() {
return textBody;
}
public String getHtmlBody() {
return htmlBody;
}
}
}

View file

@ -16,6 +16,7 @@
*/
package org.keycloak.services.resources.admin;
import com.fasterxml.jackson.core.type.TypeReference;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.BadRequestException;
@ -29,6 +30,7 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.PemUtils;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Event;
import org.keycloak.events.EventQuery;
import org.keycloak.events.EventStoreProvider;
@ -50,6 +52,7 @@ import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.UserCache;
@ -102,9 +105,9 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.PatternSyntaxException;
import static org.keycloak.models.utils.StripSecretsUtils.stripForExport;
import static org.keycloak.util.JsonSerialization.readValue;
/**
* Base resource class for the admin REST api of one realm
@ -811,6 +814,35 @@ public class RealmAdminResource {
return result ? Response.noContent().build() : ErrorResponse.error("LDAP test error", Response.Status.BAD_REQUEST);
}
/**
* Test SMTP connection with current logged in user
*
* @param config SMTP server configuration
* @return
* @throws Exception
*/
@Path("testSMTPConnection/{config}")
@POST
@NoCache
public Response testSMTPConnection(final @PathParam("config") String config) throws Exception {
Map<String, String> settings = readValue(config, new TypeReference<Map<String, String>>() {
});
try {
UserModel user = auth.adminAuth().getUser();
if (user.getEmail() == null) {
return ErrorResponse.error("Logged in user does not have an e-mail.", Response.Status.INTERNAL_SERVER_ERROR);
}
session.getProvider(EmailTemplateProvider.class).sendSmtpTestEmail(settings, user);
} catch (Exception e) {
e.printStackTrace();
logger.errorf("Failed to send email \n %s", e.getCause());
return ErrorResponse.error("Failed to send email", Response.Status.INTERNAL_SERVER_ERROR);
}
return Response.noContent().build();
}
@Path("identity-provider")
public IdentityProvidersResource getIdentityProviderResource() {
return new IdentityProvidersResource(realm, session, this.auth, adminEvent);

View file

@ -43,6 +43,10 @@ public class GreenMailRule extends ExternalResource {
greenMail.start();
}
public void credentials(String username, String password) {
greenMail.setUser(username, password);
}
@Override
protected void after() {
if (greenMail != null) {

View file

@ -0,0 +1,122 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.admin;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.UserBuilder;
import javax.mail.internet.MimeMessage;
import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.keycloak.util.JsonSerialization.writeValueAsPrettyString;
/**
* @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>
*/
public class SMTPConnectionTest extends AbstractKeycloakTest {
@Rule
public GreenMailRule greenMailRule = new GreenMailRule();
private RealmResource realm;
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
}
@Before
public void before() {
realm = adminClient.realm("master");
List<UserRepresentation> admin = realm.users().search("admin", 0, 1);
UserRepresentation user = UserBuilder.edit(admin.get(0)).email("admin@localhost").build();
realm.users().get(user.getId()).update(user);
}
private String settings(String host, String port, String from, String auth, String ssl, String starttls,
String username, String password) throws Exception {
Map<String, String> config = new HashMap<>();
config.put("host", host);
config.put("port", port);
config.put("from", from);
config.put("auth", auth);
config.put("ssl", ssl);
config.put("starttls", starttls);
config.put("user", username);
config.put("password", password);
return writeValueAsPrettyString(config);
}
@Test
public void testWithEmptySettings() throws Exception {
Response response = realm.testSMTPConnection(settings(null, null, null, null, null, null,
null, null));
assertStatus(response, 500);
}
@Test
public void testWithProperSettings() throws Exception {
Response response = realm.testSMTPConnection(settings("127.0.0.1", "3025", "auto@keycloak.org", null, null, null,
null, null));
assertStatus(response, 204);
assertMailReceived();
}
@Test
public void testWithAuthEnabledCredentialsEmpty() throws Exception {
Response response = realm.testSMTPConnection(settings("127.0.0.1", "3025", "auto@keycloak.org", "true", null, null,
null, null));
assertStatus(response, 500);
}
@Test
public void testWithAuthEnabledValidCredentials() throws Exception {
greenMailRule.credentials("admin@localhost", "admin");
Response response = realm.testSMTPConnection(settings("127.0.0.1", "3025", "auto@keycloak.org", "true", null, null,
"admin@localhost", "admin"));
assertStatus(response, 204);
}
private void assertStatus(Response response, int status) {
assertEquals(status, response.getStatus());
response.close();
}
private void assertMailReceived() {
if (greenMailRule.getReceivedMessages().length == 1) {
try {
MimeMessage message = greenMailRule.getReceivedMessages()[0];
assertEquals("[KEYCLOAK] - SMTP test message", message.getSubject());
} catch (Exception e) {
e.printStackTrace();
}
} else {
fail("E-mail was not received");
}
}
}

View file

@ -1449,7 +1449,7 @@ module.controller('RoleDetailCtrl', function($scope, realm, role, roles, clients
$http, $location, Notifications, Dialog);
});
module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, realm, $http, $location, Dialog, Notifications) {
module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, realm, $http, $location, Dialog, Notifications, RealmSMTPConnectionTester) {
console.log('RealmSMTPSettingsCtrl');
var booleanSmtpAtts = ["auth","ssl","starttls"];
@ -1484,6 +1484,25 @@ module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, real
$scope.changed = false;
};
var initSMTPTest = function() {
return {
realm: $scope.realm.realm,
config: JSON.stringify(realm.smtpServer)
};
};
$scope.testConnection = function() {
RealmSMTPConnectionTester.send(initSMTPTest(), function() {
Notifications.success("SMTP connection successful. E-mail was sent!");
}, function(errorResponse) {
if (error.data.errorMessage) {
Notifications.error(error.data.errorMessage);
} else {
Notifications.error('Unexpected error during SMTP validation');
}
});
};
/* Convert string attributes containing a boolean to actual boolean type + convert an integer string (port) to integer. */
function typeObject(obj){
for (var att in obj){

View file

@ -320,6 +320,17 @@ module.factory('RealmLDAPConnectionTester', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/testLDAPConnection');
});
module.factory('RealmSMTPConnectionTester', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/testSMTPConnection/:config', {
realm : '@realm',
config : '@config'
}, {
send: {
method: 'POST'
}
});
});
module.service('ServerInfo', function($resource, $q, $http) {
var info = {};
var delay = $q.defer();

View file

@ -10,6 +10,9 @@
<div class="col-md-6">
<input class="form-control" id="smtpHost" type="text" ng-model="realm.smtpServer.host" placeholder="{{:: 'smtp-host' | translate}}" required>
</div>
<div class="col-sm-4">
<a class="btn btn-primary" data-ng-click="testConnection()">{{:: 'test-connection' | translate}}</a>
</div>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="smtpPort">{{:: 'port' | translate}}</label>

View file

@ -0,0 +1,5 @@
<html>
<body>
${msg("emailTestBodyHtml",realmName)}
</body>
</html>

View file

@ -1,6 +1,9 @@
emailVerificationSubject=Verify email
emailVerificationBody=Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you didn''t create this account, just ignore this message.
emailVerificationBodyHtml=<p>Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address</p><p><a href="{0}">Link to e-mail address verification</a></p><p>This link will expire within {1} minutes.</p><p>If you didn''t create this account, just ignore this message.</p>
emailTestSubject=[KEYCLOAK] - SMTP test message
emailTestBody=This is a test message
emailTestBodyHtml=<p>This is a test message</p>
identityProviderLinkSubject=Link {0}
identityProviderLinkBody=Someone wants to link your "{1}" account with "{0}" account of user {2} . If this was you, click the link below to link accounts\n\n{3}\n\nThis link will expire within {4} minutes.\n\nIf you don''t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.
identityProviderLinkBodyHtml=<p>Someone wants to link your <b>{1}</b> account with <b>{0}</b> account of user {2} . If this was you, click the link below to link accounts</p><p><a href="{3}">Link to confirm account linking</a></p><p>This link will expire within {4} minutes.</p><p>If you don''t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.</p>

View file

@ -0,0 +1 @@
${msg("emailTestBody", realmName)}