-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
added overridable email sender impl (#41)
* added overridable email sender impl * proper property name parsing * removed ServerInfoAware impl because it leaks the config to other realms * added docs to README * updated docs * added cache counter for max emails * cache key includes date for 1 day expiration * sample of cache xml. failsafe without cache configured. * formatting in readme. changed condition name to useRealmConfig for clarity. * space between variables in dockerfile
- Loading branch information
Showing
6 changed files
with
294 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<infinispan | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="urn:infinispan:config:15.0 http://www.infinispan.org/schemas/infinispan-config-15.0.xsd" | ||
xmlns="urn:infinispan:config:15.0"> | ||
|
||
<cache-container name="keycloak"> | ||
<transport lock-timeout="60000" stack="udp"/> | ||
<local-cache name="realms" simple-cache="true"> | ||
<encoding> | ||
<key media-type="application/x-java-object"/> | ||
<value media-type="application/x-java-object"/> | ||
</encoding> | ||
<memory max-count="10000"/> | ||
</local-cache> | ||
<local-cache name="users" simple-cache="true"> | ||
<encoding> | ||
<key media-type="application/x-java-object"/> | ||
<value media-type="application/x-java-object"/> | ||
</encoding> | ||
<memory max-count="10000"/> | ||
</local-cache> | ||
<distributed-cache name="sessions" owners="1"> | ||
<expiration lifespan="-1"/> | ||
<memory max-count="10000"/> | ||
</distributed-cache> | ||
<distributed-cache name="authenticationSessions" owners="2"> | ||
<expiration lifespan="-1"/> | ||
</distributed-cache> | ||
<distributed-cache name="offlineSessions" owners="1"> | ||
<expiration lifespan="-1"/> | ||
<memory max-count="10000"/> | ||
</distributed-cache> | ||
<distributed-cache name="clientSessions" owners="1"> | ||
<expiration lifespan="-1"/> | ||
<memory max-count="10000"/> | ||
</distributed-cache> | ||
<distributed-cache name="offlineClientSessions" owners="1"> | ||
<expiration lifespan="-1"/> | ||
<memory max-count="10000"/> | ||
</distributed-cache> | ||
<distributed-cache name="loginFailures" owners="2"> | ||
<expiration lifespan="-1"/> | ||
</distributed-cache> | ||
<local-cache name="authorization" simple-cache="true"> | ||
<encoding> | ||
<key media-type="application/x-java-object"/> | ||
<value media-type="application/x-java-object"/> | ||
</encoding> | ||
<memory max-count="10000"/> | ||
</local-cache> | ||
<replicated-cache name="work"> | ||
<expiration lifespan="-1"/> | ||
</replicated-cache> | ||
<!-- custom for counters --> | ||
<replicated-cache name="counterCache"> | ||
<expiration lifespan="-1"/> | ||
</replicated-cache> | ||
<local-cache name="keys" simple-cache="true"> | ||
<encoding> | ||
<key media-type="application/x-java-object"/> | ||
<value media-type="application/x-java-object"/> | ||
</encoding> | ||
<expiration max-idle="3600000"/> | ||
<memory max-count="1000"/> | ||
</local-cache> | ||
<distributed-cache name="actionTokens" owners="2"> | ||
<encoding> | ||
<key media-type="application/x-java-object"/> | ||
<value media-type="application/x-java-object"/> | ||
</encoding> | ||
<expiration max-idle="-1" lifespan="-1" interval="300000"/> | ||
<memory max-count="-1"/> | ||
</distributed-cache> | ||
</cache-container> | ||
</infinispan> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,5 +12,7 @@ services: | |
- 8080:8080 | ||
volumes: | ||
- ./target/keycloak-themes-0.34-SNAPSHOT.jar:/opt/keycloak/providers/keycloak-themes.jar | ||
- ${PWD}/mnt_themes:/opt/keycloak/jarthemes | ||
entrypoint: /opt/keycloak/bin/kc.sh --verbose start-dev --spi-email-template-provider=freemarker-plus-mustache --spi-email-template-freemarker-plus-mustache-enabled=true --spi-theme-ext-theme-jar-folder-dir=/opt/keycloak/jarthemes --spi-theme-cache-themes=false | ||
- ${PWD}/jarthemes:/opt/keycloak/jarthemes | ||
- ./conf/cache-ispn-custom.xml:/opt/keycloak/conf/cache-ispn-custom.xml | ||
entrypoint: /opt/keycloak/bin/kc.sh --verbose start-dev --cache-config-file=cache-ispn-custom.xml --spi-email-template-provider=freemarker-plus-mustache --spi-email-template-freemarker-plus-mustache-enabled=true --spi-email-sender-provider=ext-email-override --spi-email-sender-ext-email-override-enabled=true --spi-email-sender-ext-email-override-host=smtp.someserver.com --spi-email-sender-ext-email-override-auth=true [email protected] --spi-email-sender-ext-email-override-port=587 --spi-email-sender-ext-email-override-starttls=true --spi-email-sender-ext-email-override-user=someuser --spi-email-sender-ext-email-override-password=somepass --spi-email-sender-ext-email-override-max-emails=200 --spi-theme-ext-theme-jar-folder-dir=/opt/keycloak/jarthemes --spi-theme-cache-themes=false | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
src/main/java/io/phasetwo/keycloak/email/OverridableEmailSenderProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
package io.phasetwo.keycloak.email; | ||
|
||
import java.text.SimpleDateFormat; | ||
import java.util.Date; | ||
import java.util.Map; | ||
import java.util.concurrent.TimeUnit; | ||
import lombok.extern.jbosslog.JBossLog; | ||
import org.infinispan.Cache; | ||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider; | ||
import org.keycloak.email.DefaultEmailSenderProvider; | ||
import org.keycloak.email.EmailException; | ||
import org.keycloak.models.KeycloakSession; | ||
import org.keycloak.models.UserModel; | ||
|
||
@JBossLog | ||
public class OverridableEmailSenderProvider extends DefaultEmailSenderProvider { | ||
|
||
private final KeycloakSession session; | ||
private final Map<String, String> conf; | ||
private final Integer maxEmails; | ||
private final String cacheKey; | ||
private Cache<String, Integer> counterCache; | ||
|
||
public OverridableEmailSenderProvider( | ||
KeycloakSession session, Map<String, String> conf, Integer maxEmails) { | ||
super(session); | ||
this.session = session; | ||
this.conf = conf; | ||
this.maxEmails = maxEmails; | ||
this.cacheKey = getCacheKey(); | ||
try { | ||
this.counterCache = | ||
session.getProvider(InfinispanConnectionProvider.class).getCache("counterCache", true); | ||
} catch (Exception e) { | ||
log.warnf("Error loading counterCache %s", e); | ||
} | ||
} | ||
|
||
private boolean useRealmConfig(Map<String, String> config) { | ||
return (!config.isEmpty() && config.containsKey("host")); | ||
} | ||
|
||
private String getCacheKey() { | ||
if (session.getContext().getRealm() != null) { | ||
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); | ||
return String.format( | ||
"ext-email-override-emailCounter-%s-%s", | ||
session.getContext().getRealm().getName(), formatter.format(new Date())); | ||
} else { | ||
return null; | ||
} | ||
} | ||
|
||
private boolean canSend() { | ||
if (cacheKey == null) return true; | ||
Integer count = counterCache.get(cacheKey); | ||
log.infof("Count for %s is %d / %d", cacheKey, count, maxEmails); | ||
if (count == null || count <= maxEmails) return true; | ||
else return false; | ||
} | ||
|
||
private Integer increment() { | ||
if (cacheKey == null) return 0; | ||
else | ||
return counterCache.compute( | ||
cacheKey, (key, value) -> (value == null) ? 1 : value + 1, 1, TimeUnit.DAYS); | ||
} | ||
|
||
@Override | ||
public void send( | ||
Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) | ||
throws EmailException { | ||
if (useRealmConfig(config)) { | ||
log.debug("Using customer override email sender"); | ||
super.send(config, user, subject, textBody, htmlBody); | ||
} else { | ||
if (canSend()) { | ||
super.send(conf, user, subject, textBody, htmlBody); | ||
Integer count = increment(); | ||
log.infof("Email count %d for %s", count, cacheKey); | ||
} else { | ||
log.infof("Unable to send email for limit %d %s", maxEmails, cacheKey); | ||
} | ||
} | ||
} | ||
|
||
@Override | ||
public void send( | ||
Map<String, String> config, String address, String subject, String textBody, String htmlBody) | ||
throws EmailException { | ||
if (useRealmConfig(config)) { | ||
log.debug("Using customer override email sender"); | ||
super.send(config, address, subject, textBody, htmlBody); | ||
} else { | ||
if (canSend()) { | ||
super.send(conf, address, subject, textBody, htmlBody); | ||
Integer count = increment(); | ||
log.infof("Email count %d for %s", count, cacheKey); | ||
} else { | ||
log.infof("Unable to send email for limit %d %s", maxEmails, cacheKey); | ||
} | ||
} | ||
} | ||
} |
70 changes: 70 additions & 0 deletions
70
src/main/java/io/phasetwo/keycloak/email/OverridableEmailSenderProviderFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package io.phasetwo.keycloak.email; | ||
|
||
import com.google.auto.service.AutoService; | ||
import com.google.common.base.Strings; | ||
import com.google.common.collect.ImmutableMap; | ||
import java.util.Map; | ||
import lombok.extern.jbosslog.JBossLog; | ||
import org.keycloak.Config; | ||
import org.keycloak.email.EmailSenderProviderFactory; | ||
import org.keycloak.models.KeycloakSession; | ||
import org.keycloak.models.KeycloakSessionFactory; | ||
|
||
@JBossLog | ||
@AutoService(EmailSenderProviderFactory.class) | ||
public class OverridableEmailSenderProviderFactory implements EmailSenderProviderFactory { | ||
|
||
private Integer maxEmails; | ||
private Map<String, String> conf; | ||
|
||
@Override | ||
public OverridableEmailSenderProvider create(KeycloakSession session) { | ||
return new OverridableEmailSenderProvider(session, conf, maxEmails); | ||
} | ||
|
||
public static final String[] PROPERTY_NAMES = { | ||
"host", | ||
"auth", | ||
"ssl", | ||
"starttls", | ||
"port", | ||
"from", | ||
"fromDisplayName", | ||
"replyTo", | ||
"replyToDisplayName", | ||
"envelopeFrom", | ||
"user", | ||
"password" | ||
}; | ||
|
||
@Override | ||
public void init(Config.Scope config) { | ||
log.info("Initializing config for email sender."); | ||
this.maxEmails = config.getInt("maxEmails", 100); | ||
log.infof("maxEmails set to %d", this.maxEmails); | ||
String host = config.get("host"); | ||
if (!Strings.isNullOrEmpty(host)) { // TODO better test than this | ||
ImmutableMap.Builder<String, String> builder = ImmutableMap.builder(); | ||
for (String name : PROPERTY_NAMES) { | ||
String v = config.get(name); | ||
if (v != null) { | ||
builder.put(name, v); | ||
} | ||
} | ||
this.conf = builder.build(); | ||
} else { | ||
this.conf = ImmutableMap.of(); | ||
} | ||
} | ||
|
||
@Override | ||
public void postInit(KeycloakSessionFactory factory) {} | ||
|
||
@Override | ||
public void close() {} | ||
|
||
@Override | ||
public String getId() { | ||
return "ext-email-override"; | ||
} | ||
} |