Merge pull request #4259 from stianst/abstractj-KEYCLOAK-4444
KEYCLOAK-4444
This commit is contained in:
commit
56c5996aff
14 changed files with 293 additions and 21 deletions
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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){
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<html>
|
||||
<body>
|
||||
${msg("emailTestBodyHtml",realmName)}
|
||||
</body>
|
||||
</html>
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
${msg("emailTestBody", realmName)}
|
Loading…
Reference in a new issue