@@ -9,6 +9,7 @@ import bubble.dao.account.AccountDAO; | |||
import bubble.dao.account.AccountOwnedEntityDAO; | |||
import bubble.dao.cloud.BubbleNetworkDAO; | |||
import bubble.dao.cloud.CloudServiceDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.bill.AccountPlan; | |||
import bubble.model.bill.Bill; | |||
import bubble.model.bill.BubblePlan; | |||
@@ -18,8 +19,10 @@ import bubble.model.cloud.CloudService; | |||
import bubble.model.cloud.HostnameValidationResult; | |||
import bubble.notify.payment.PaymentValidationResult; | |||
import bubble.server.BubbleConfiguration; | |||
import bubble.service.account.SyncAccountService; | |||
import bubble.service.bill.RefundService; | |||
import bubble.service.cloud.NetworkService; | |||
import lombok.NonNull; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Repository; | |||
@@ -166,9 +169,20 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> { | |||
paymentDriver.purchase(accountPlanUuid, paymentMethodUuid, billUuid); | |||
}); | |||
} | |||
final var account = accountDAO.findByUuid(accountPlan.getAccount()); | |||
configuration.getBean(SyncAccountService.class).syncPlan(account, accountPlan); | |||
return super.postCreate(accountPlan, context); | |||
} | |||
@Override public AccountPlan postUpdate(@NonNull final AccountPlan accountPlan, @NonNull final Object context) { | |||
final var account = accountDAO.findByUuid(accountPlan.getAccount()); | |||
configuration.getBean(SyncAccountService.class).syncPlan(account, accountPlan); | |||
return super.postUpdate(accountPlan, context); | |||
} | |||
@Override public void delete(String uuid) { | |||
final AccountPlan accountPlan = findByUuid(uuid); | |||
if (accountPlan == null) return; | |||
@@ -193,6 +207,14 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> { | |||
} | |||
} | |||
@Override public void forceDelete(String uuid) { super.delete(uuid); } | |||
@Override public void forceDelete(String uuid) { | |||
final AccountPlan accountPlan = findByUuid(uuid); | |||
final Account account = accountDAO.findByUuid(accountPlan.getAccount()); | |||
super.delete(uuid); | |||
configuration.getBean(SyncAccountService.class).syncForceDeletedPlan(account, accountPlan.getName()); | |||
} | |||
public void forceDeleteWithoutSync(String uuid) { super.delete(uuid); } | |||
} |
@@ -174,6 +174,9 @@ public class AccountPlan extends IdentifiableBase implements HasNetwork { | |||
@Transient @Getter @Setter private transient Boolean sendMetrics = null; | |||
public boolean sendMetrics () { return sendMetrics == null || sendMetrics; } | |||
@JsonIgnore @Transient @Getter @Setter private Boolean skipSync; | |||
public boolean skipSync() { return bool(skipSync); } | |||
public BubbleNetwork bubbleNetwork(Account account, | |||
BubbleDomain domain, | |||
BubblePlan plan, | |||
@@ -5,12 +5,14 @@ | |||
package bubble.notify; | |||
import bubble.dao.account.AccountDAO; | |||
import bubble.dao.bill.AccountPlanDAO; | |||
import bubble.dao.cloud.BubbleNodeDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.bill.AccountPlan; | |||
import bubble.model.cloud.AnsibleInstallType; | |||
import bubble.model.cloud.BubbleNode; | |||
import bubble.model.cloud.notify.ReceivedNotification; | |||
import bubble.service.account.SyncAccountNotification; | |||
import lombok.NonNull; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
@@ -22,34 +24,74 @@ public class NotificationHandler_sync_account extends ReceivedNotificationHandle | |||
@Autowired private BubbleNodeDAO nodeDAO; | |||
@Autowired private AccountDAO accountDAO; | |||
@Autowired private AccountPlanDAO accountPlanDAO; | |||
@Override public void handleNotification(ReceivedNotification n) { | |||
final BubbleNode node = nodeDAO.findByUuid(n.getFromNode()); | |||
@Override | |||
public void handleNotification(ReceivedNotification n) { | |||
final var node = nodeDAO.findByUuid(n.getFromNode()); | |||
if (node == null) { | |||
log.warn("sync_account: node not found: "+n.getFromNode()); | |||
log.warn("sync_account: node not found: " + n.getFromNode()); | |||
return; | |||
} | |||
final var notification = json(n.getPayloadJson(), SyncAccountNotification.class); | |||
final var localAccount = accountDAO.findByUuid(notification.getAccountUuid()); | |||
if (localAccount == null) { | |||
reportError("sync_account: localAccount not found: " + notification.getAccountUuid()); | |||
return; | |||
} | |||
if (!localAccount.sync()) { | |||
log.info("sync_account: localAccount " + localAccount.getName() + " has sync disabled, no sync done"); | |||
return; | |||
} | |||
if (notification.getHashedPassword().isPresent()) { | |||
syncAccountPassword(localAccount, notification.getHashedPassword().get()); | |||
} | |||
if (notification.getAccountPlan().isPresent()) { | |||
syncAccountPlan(localAccount.getUuid(), notification.getAccountPlan().get()); | |||
} | |||
if (notification.getForceDeletedAccountPlanName().isPresent()) { | |||
syncForceDelAccountPlan(localAccount.getUuid(), notification.getForceDeletedAccountPlanName().get()); | |||
} | |||
} | |||
private void syncAccountPassword(@NonNull final Account localAccount, | |||
@NonNull final String incomingHashedPassword) { | |||
localAccount.getHashedPassword().setHashedPassword(incomingHashedPassword); | |||
// if we are a node, set skipSync so we don't get caught in an infinite loop | |||
// (the node would notify the sage, which would notify the node, ad infinitum) | |||
localAccount.setSkipSync(configuration.getThisNetwork().getInstallType() == AnsibleInstallType.node); | |||
// update password, if we are a sage, this will notify all networks of password change | |||
accountDAO.update(localAccount); | |||
} | |||
private void syncAccountPlan(@NonNull final String accountUuid, @NonNull final AccountPlan incomingPlan) { | |||
final var localPlan = accountPlanDAO.findByAccountAndId(accountUuid, incomingPlan.getName()); | |||
if (localPlan == null) { | |||
// create new localPlan | |||
final var newPlan = new AccountPlan(incomingPlan); | |||
newPlan.setSkipSync(configuration.getThisNetwork().getInstallType() == AnsibleInstallType.node); | |||
accountPlanDAO.create(newPlan); | |||
} else { | |||
// update localPlan | |||
localPlan.update(incomingPlan); | |||
localPlan.setSkipSync(configuration.getThisNetwork().getInstallType() == AnsibleInstallType.node); | |||
accountPlanDAO.update(localPlan); | |||
} | |||
} | |||
private void syncForceDelAccountPlan(@NonNull final String accountUuid, @NonNull final String planName) { | |||
final var localPlan = accountPlanDAO.findByAccountAndId(accountUuid, planName); | |||
if (configuration.getThisNetwork().getInstallType() == AnsibleInstallType.node) { | |||
accountPlanDAO.forceDeleteWithoutSync(localPlan.getUuid()); | |||
} else { | |||
final SyncAccountNotification notification = json(n.getPayloadJson(), SyncAccountNotification.class); | |||
final Account account = accountDAO.findByUuid(notification.getAccountUuid()); | |||
if (account == null) { | |||
reportError("sync_account: account not found: "+notification.getAccountUuid()); | |||
return; | |||
} | |||
if (!account.sync()) { | |||
log.info("sync_account: account "+account.getName()+" has sync disabled, not synchronizing"); | |||
return; | |||
} | |||
account.getHashedPassword().setHashedPassword(notification.getHashedPassword()); | |||
// if we are a node, set skipSync so we don't get caught in an infinite loop | |||
// (the node would notify the sage, which would notify the node, ad infinitum) | |||
if (configuration.getThisNetwork().getInstallType() == AnsibleInstallType.node) { | |||
account.setSkipSync(true); | |||
} | |||
// update password, if we are a sage, this will notify all networks of password change | |||
accountDAO.update(account); | |||
accountPlanDAO.forceDelete(localPlan.getUuid()); | |||
} | |||
} | |||
@@ -7,12 +7,14 @@ package bubble.service.account; | |||
import bubble.dao.cloud.BubbleNetworkDAO; | |||
import bubble.dao.cloud.BubbleNodeDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.bill.AccountPlan; | |||
import bubble.model.cloud.AnsibleInstallType; | |||
import bubble.model.cloud.BubbleNetwork; | |||
import bubble.model.cloud.BubbleNetworkState; | |||
import bubble.model.cloud.BubbleNode; | |||
import bubble.server.BubbleConfiguration; | |||
import bubble.service.notify.NotificationService; | |||
import lombok.NonNull; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Service; | |||
@@ -28,51 +30,62 @@ public class StandardSyncAccountService implements SyncAccountService { | |||
@Autowired private NotificationService notificationService; | |||
@Autowired private BubbleConfiguration configuration; | |||
public void syncAccount(Account account) { | |||
public void syncAccount(@NonNull final Account account) { | |||
sync(account, new SyncAccountNotification(account)); | |||
} | |||
public void syncPlan(@NonNull final Account account, @NonNull final AccountPlan accountPlan) { | |||
sync(account, new SyncAccountNotification(account.getUuid(), accountPlan)); | |||
} | |||
public void syncForceDeletedPlan(@NonNull final Account account, @NonNull final String accountPlanName) { | |||
sync(account, new SyncAccountNotification(account.getUuid(), accountPlanName)); | |||
} | |||
private void sync(@NonNull final Account account, @NonNull final SyncAccountNotification notification) { | |||
final BubbleNetwork thisNetwork = configuration.getThisNetwork(); | |||
if (thisNetwork == null) { | |||
// should never happen | |||
log.warn("syncAccount: thisNetwork was null, sync_account is impossible"); | |||
log.warn("sync: thisNetwork was null, sync_account is impossible"); | |||
return; | |||
} | |||
if (!account.admin()) { | |||
log.info("syncAccount: not syncing non-admin password"); | |||
log.info("sync: not syncing non-admin account"); | |||
return; | |||
} | |||
if (!account.sync()) { | |||
log.info("syncAccount: password sync disabled for account: "+account.getName()); | |||
log.info("sync: account sync disabled for account: "+account.getName()); | |||
return; | |||
} | |||
final AnsibleInstallType installType = thisNetwork.getInstallType(); | |||
final SyncAccountNotification notification = new SyncAccountNotification(account); | |||
if (installType == AnsibleInstallType.sage) { | |||
// changing password on sage, notify all bubbles launched by user that have syncAccount == true | |||
// changing account on sage, notify all bubbles launched by user that have syncAccount == true | |||
for (BubbleNetwork network : networkDAO.findByAccount(account.getUuid())) { | |||
if (network.getState() != BubbleNetworkState.running) continue; | |||
if (!network.syncAccount()) continue; | |||
for (BubbleNode node : nodeDAO.findByNetwork(network.getUuid())) { | |||
if (node.getUuid().equals(configuration.getThisNode().getUuid())) { | |||
log.info("syncAccount: not notifying self"); | |||
log.info("sync: not notifying self"); | |||
continue; | |||
} | |||
log.info("syncAccount: sending sync_account notification from sage to node: "+node.id()); | |||
log.info("sync: sending sync_account notification from sage to node: "+node.id()); | |||
notificationService.notify(node, sync_account, notification); | |||
} | |||
} | |||
} else if (installType == AnsibleInstallType.node) { | |||
if (!thisNetwork.syncAccount()) { | |||
log.info("syncAccount: disabled for node, not sending sync_account notification"); | |||
log.info("sync: disabled for node, not sending sync_account notification"); | |||
return; | |||
} | |||
// changing password on node, notify sage, which will then notify all bubbles launched by user that have | |||
// changing account on node, notify sage, which will then notify all bubbles launched by user that have | |||
// syncAccount == true | |||
log.info("syncAccount: sending sync_account notification from node to sage: "+configuration.getSageNode()); | |||
log.info("sync: sending sync_account notification from node to sage: "+configuration.getSageNode()); | |||
notificationService.notify(configuration.getSageNode(), sync_account, notification); | |||
} else { | |||
reportError("syncAccount("+account.getEmail()+"/"+account.getUuid()+"): invalid installType: "+installType); | |||
reportError("sync("+account.getEmail()+"/"+account.getUuid()+"): invalid installType: "+installType); | |||
} | |||
} | |||
@@ -5,20 +5,35 @@ | |||
package bubble.service.account; | |||
import bubble.model.account.Account; | |||
import bubble.model.bill.AccountPlan; | |||
import lombok.Getter; | |||
import lombok.NoArgsConstructor; | |||
import lombok.NonNull; | |||
import lombok.Setter; | |||
import lombok.experimental.Accessors; | |||
import java.util.Optional; | |||
@NoArgsConstructor @Accessors(chain=true) | |||
public class SyncAccountNotification { | |||
@Getter @Setter private String accountUuid; | |||
@Getter @Setter private String hashedPassword; | |||
@Getter @Setter private Optional<String> hashedPassword = Optional.empty(); | |||
@Getter @Setter private Optional<AccountPlan> accountPlan = Optional.empty(); | |||
@Getter @Setter private Optional<String> forceDeletedAccountPlanName = Optional.empty(); | |||
public SyncAccountNotification(Account account) { | |||
public SyncAccountNotification(@NonNull final Account account) { | |||
this.accountUuid = account.getUuid(); | |||
this.hashedPassword = account.getHashedPassword().getHashedPassword(); | |||
this.hashedPassword = Optional.of(account.getHashedPassword().getHashedPassword()); | |||
} | |||
public SyncAccountNotification(@NonNull final String accountUuid, @NonNull final AccountPlan accountPlan) { | |||
this.accountUuid = accountUuid; | |||
this.accountPlan = Optional.of(accountPlan); | |||
} | |||
public SyncAccountNotification(@NonNull final String accountUuid, @NonNull final String forceDeletedPlanUuid) { | |||
this.accountUuid = accountUuid; | |||
this.forceDeletedAccountPlanName = Optional.of(forceDeletedPlanUuid); | |||
} | |||
} |
@@ -5,9 +5,13 @@ | |||
package bubble.service.account; | |||
import bubble.model.account.Account; | |||
import bubble.model.bill.AccountPlan; | |||
import lombok.NonNull; | |||
public interface SyncAccountService { | |||
void syncAccount(Account account); | |||
void syncAccount(@NonNull final Account account); | |||
void syncPlan(@NonNull final Account account, @NonNull final AccountPlan accountPlan); | |||
void syncForceDeletedPlan(@NonNull final Account account, @NonNull final String accountPlanName); | |||
} |
@@ -5,7 +5,9 @@ | |||
package bubble.service_dbfilter; | |||
import bubble.model.account.Account; | |||
import bubble.model.bill.AccountPlan; | |||
import bubble.service.account.SyncAccountService; | |||
import lombok.NonNull; | |||
import org.springframework.stereotype.Service; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.notSupported; | |||
@@ -13,6 +15,13 @@ import static org.cobbzilla.util.daemon.ZillaRuntime.notSupported; | |||
@Service | |||
public class DbFilterSyncAccountService implements SyncAccountService { | |||
@Override public void syncAccount(Account account) { notSupported("syncAccount"); } | |||
@Override public void syncAccount(@NonNull final Account account) { notSupported("syncAccount"); } | |||
@Override public void syncPlan(@NonNull final Account account, @NonNull final AccountPlan accountPlan) { | |||
notSupported("syncPlan"); | |||
} | |||
@Override public void syncForceDeletedPlan(@NonNull final Account account, @NonNull final String accountPlanName) { | |||
notSupported("syncForceDeletedPlan"); | |||
} | |||
} |