diff --git a/bubble-server/pom.xml b/bubble-server/pom.xml index bb0f8330..4ed177c5 100644 --- a/bubble-server/pom.xml +++ b/bubble-server/pom.xml @@ -276,6 +276,11 @@ + + com.sendgrid + sendgrid-java + 4.6.5 + diff --git a/bubble-server/src/main/java/bubble/cloud/CloudServiceDriver.java b/bubble-server/src/main/java/bubble/cloud/CloudServiceDriver.java index 5f1e7abd..af47b474 100644 --- a/bubble-server/src/main/java/bubble/cloud/CloudServiceDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/CloudServiceDriver.java @@ -10,6 +10,7 @@ import bubble.model.cloud.CloudService; import bubble.server.BubbleConfiguration; import com.fasterxml.jackson.databind.JsonNode; import com.github.jknack.handlebars.Handlebars; +import lombok.NonNull; import org.cobbzilla.util.handlebars.HandlebarsUtil; import org.cobbzilla.util.http.HttpResponseBean; import org.cobbzilla.util.http.HttpUtil; @@ -28,6 +29,18 @@ public interface CloudServiceDriver { default boolean disableDelegation () { return false; } + @NonNull default CloudService setupDelegatedCloudService(@NonNull final BubbleConfiguration configuration, + @NonNull final CloudService parentService, + @NonNull final CloudService delegatedService) { + return delegatedService.setDelegated(parentService.getUuid()) + .setCredentials(CloudCredentials.delegate(configuration.getThisNode(), configuration)) + .setTemplate(false); + } + + default void postServiceDelete(@NonNull final CloudService service) { + // noop + } + void setConfig(JsonNode json, CloudService cloudService); static T setupDriver(BubbleConfiguration configuration, T driver) { diff --git a/bubble-server/src/main/java/bubble/cloud/email/SendgridSmtpEmailDriver.java b/bubble-server/src/main/java/bubble/cloud/email/SendgridSmtpEmailDriver.java new file mode 100644 index 00000000..ea1ba7b5 --- /dev/null +++ b/bubble-server/src/main/java/bubble/cloud/email/SendgridSmtpEmailDriver.java @@ -0,0 +1,116 @@ +package bubble.cloud.email; + +import bubble.dao.account.AccountDAO; +import bubble.dao.cloud.CloudServiceDAO; +import bubble.model.account.Account; +import bubble.model.cloud.CloudCredentials; +import bubble.model.cloud.CloudService; +import bubble.server.BubbleConfiguration; +import com.sendgrid.Method; +import com.sendgrid.Request; +import com.sendgrid.Response; +import com.sendgrid.SendGrid; +import lombok.*; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.IOException; + +import static bubble.cloud.storage.StorageCryptStream.MIN_DISTINCT_LENGTH; +import static bubble.cloud.storage.StorageCryptStream.MIN_KEY_LENGTH; +import static java.net.HttpURLConnection.HTTP_NO_CONTENT; +import static java.net.HttpURLConnection.HTTP_OK; +import static org.cobbzilla.util.daemon.ZillaRuntime.die; +import static org.cobbzilla.util.json.JsonUtil.EMPTY_JSON; +import static org.cobbzilla.util.json.JsonUtil.json; +import static org.cobbzilla.util.security.CryptoUtil.generatePassword; +import static org.cobbzilla.util.string.StringUtil.repeat; + +public class SendgridSmtpEmailDriver extends SmtpEmailDriver { + + private static final String PARAM_PARENT_SERVICE = "parentService"; + + @Autowired private AccountDAO accountDAO; + @Autowired private CloudServiceDAO serviceDAO; + + @Override protected boolean isServiceCompatible() { return this.config.getHost().equals(SENDGRID_SMTP); } + + /** + * Build username which will be used for Subuser created on SendGrid's service for specified Account's object. + */ + @NonNull private String sgUsername(@NonNull final CloudService delegatedService) { + return delegatedService.getShortId(); + } + + @Override @NonNull public CloudService setupDelegatedCloudService(@NonNull final BubbleConfiguration configuration, + @NonNull final CloudService parentService, + @NonNull final CloudService delegatedService) { + final CloudCredentials parentCredentials = parentService.getCredentials(); + if (parentService.delegated() || !parentCredentials.getParam(PARAM_HOST).contains(".sendgrid.net")) { + return super.setupDelegatedCloudService(configuration, parentService, delegatedService); + } + + final SendGrid sg = new SendGrid(parentCredentials.getParam(PARAM_PASSWORD)); + final Account accountWithDelegate = accountDAO.findByUuid(delegatedService.getAccount()); + final String user = sgUsername(delegatedService); + String password = generatePassword(MIN_KEY_LENGTH, MIN_DISTINCT_LENGTH); + final CreateSubuserRequest data = new CreateSubuserRequest(user, accountWithDelegate.getEmail(), password, + new String[]{}); + final Request req = new Request(); + req.setMethod(Method.POST); + req.setEndpoint("subusers"); + req.setBody(json(data)); + + final Response res; + try { + res = sg.api(req); + } catch (IOException e) { + return die("Cannot create SendGrid Subuser", e); + } + if (res.getStatusCode() != HTTP_OK) { + return die("Wrong response when creating SendGrid Subuser: " + res.getStatusCode() + " : " + res.getBody()); + } + + delegatedService.setDelegated(null).setTemplate(false); + delegatedService.getCredentials() + .setParam(PARAM_USER, user) + .setParam(PARAM_PASSWORD, password) + .setParam(PARAM_PARENT_SERVICE, parentService.getUuid()); + password = repeat("x", MIN_KEY_LENGTH); // Override password (in memory) just in case + return delegatedService; + } + + @Override public void postServiceDelete(@NonNull final CloudService service) { + final String parentServiceUuid = service.getCredentials().getParam(PARAM_PARENT_SERVICE); + if (parentServiceUuid == null) return; + + final CloudService parentService = serviceDAO.findByUuid(parentServiceUuid); + if (parentService == null) return; + + final SendGrid sg = new SendGrid(parentService.getCredentials().getParam(PARAM_PASSWORD)); + final String sgUserToDelete = sgUsername(service); + + final Request req = new Request(); + req.setMethod(Method.DELETE); + req.setEndpoint("subusers/" + sgUserToDelete); + req.setBody(EMPTY_JSON); + + final Response res; + try { + res = sg.api(req); + } catch (IOException e) { + die("Cannot delete SendGrid Subuser " + sgUserToDelete, e); + return; + } + if (res.getStatusCode() != HTTP_NO_CONTENT) { + die("Wrong response when creating SendGrid Subuser: " + res.getStatusCode() + " : " + res.getBody()); + } + } + + @AllArgsConstructor @NoArgsConstructor + private class CreateSubuserRequest { + @Getter @Setter private String username; + @Getter @Setter private String email; + @Getter @Setter private String password; + @Getter @Setter private String[] ips; + } +} diff --git a/bubble-server/src/main/java/bubble/cloud/email/SmtpEmailDriver.java b/bubble-server/src/main/java/bubble/cloud/email/SmtpEmailDriver.java index 16916e78..1e757428 100644 --- a/bubble-server/src/main/java/bubble/cloud/email/SmtpEmailDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/email/SmtpEmailDriver.java @@ -10,19 +10,30 @@ import bubble.cloud.email.mock.MockMailSender; import bubble.model.account.Account; import bubble.model.account.AccountContact; import bubble.model.account.message.AccountMessage; +import bubble.model.cloud.CloudService; import bubble.server.BubbleConfiguration; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.cobbzilla.mail.sender.SmtpMailSender; import org.springframework.beans.factory.annotation.Autowired; +import java.util.ArrayList; +import java.util.List; + +import static org.cobbzilla.util.daemon.ZillaRuntime.die; + @Slf4j public class SmtpEmailDriver extends CloudServiceDriverBase implements EmailServiceDriver { + protected static final String SENDGRID_SMTP = "smtp.sendgrid.net"; + + private static final List SEPARATE_DRIVERS_SMTPS = new ArrayList<>(); + static { SEPARATE_DRIVERS_SMTPS.add(SENDGRID_SMTP); } - private static final String PARAM_USER = "user"; - private static final String PARAM_PASSWORD = "password"; - private static final String PARAM_HOST = "host"; - private static final String PARAM_PORT = "port"; + protected static final String PARAM_USER = "user"; + protected static final String PARAM_PASSWORD = "password"; + protected static final String PARAM_HOST = "host"; + protected static final String PARAM_PORT = "port"; @Autowired @Getter protected BubbleConfiguration configuration; @@ -38,6 +49,13 @@ public class SmtpEmailDriver extends CloudServiceDriverBase i return smtpSender; } + @Override public void setConfig(JsonNode json, CloudService cloudService) { + super.setConfig(json, cloudService); + if (!isServiceCompatible()) die("Specified SmtpEmailDriver is not compatible with given config"); + } + + protected boolean isServiceCompatible() { return !SEPARATE_DRIVERS_SMTPS.contains(this.config.getHost()); } + @Override public boolean send(Account account, AccountMessage message, AccountContact contact) { return EmailServiceDriver.send(this, account, message, contact); } diff --git a/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java b/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java index 45746fe0..271a69f2 100644 --- a/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java +++ b/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java @@ -229,12 +229,11 @@ public class AccountDAO extends AbstractCRUDDAO implements SqlViewSearc .setTemplate(false); } else { - return accountEntity.setDelegated(parentEntity.getUuid()) - .setCredentials(CloudCredentials.delegate(configuration.getThisNode(), configuration)) - .setTemplate(false); + return driver.setupDelegatedCloudService(configuration, parentEntity, accountEntity); } } } + @Override public void postCreate(CloudService parentEntity, CloudService accountEntity) { clouds.put(parentEntity.getUuid(), accountEntity); } diff --git a/bubble-server/src/main/java/bubble/model/cloud/CloudService.java b/bubble-server/src/main/java/bubble/model/cloud/CloudService.java index 9379ac8f..52003d73 100644 --- a/bubble-server/src/main/java/bubble/model/cloud/CloudService.java +++ b/bubble-server/src/main/java/bubble/model/cloud/CloudService.java @@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.NonNull; import lombok.Setter; import lombok.experimental.Accessors; import org.cobbzilla.util.collection.ArrayUtil; @@ -342,4 +343,8 @@ public class CloudService extends IdentifiableBaseParentEntity implements Accoun } return errors; } + + @PostRemove void onPostRemove(@NonNull final CloudService service) { + service.getDriver().postServiceDelete(service); + } } diff --git a/bubble-server/src/main/resources/models/defaults/cloudService.json b/bubble-server/src/main/resources/models/defaults/cloudService.json index aa718d66..cf410649 100644 --- a/bubble-server/src/main/resources/models/defaults/cloudService.json +++ b/bubble-server/src/main/resources/models/defaults/cloudService.json @@ -72,7 +72,7 @@ { "name": "SmtpServer", "type": "email", - "driverClass": "bubble.cloud.email.SmtpEmailDriver", + "driverClass": "{{BUBBLE_SMTP_DRIVER}}", "driverConfig": { "tlsEnabled": true },