diff --git a/bubble-server/src/main/java/bubble/cloud/compute/NodeReaper.java b/bubble-server/src/main/java/bubble/cloud/compute/NodeReaper.java index 4b4d6093..21107431 100644 --- a/bubble-server/src/main/java/bubble/cloud/compute/NodeReaper.java +++ b/bubble-server/src/main/java/bubble/cloud/compute/NodeReaper.java @@ -62,8 +62,8 @@ public class NodeReaper extends SimpleDaemon { private void processNode(@NonNull final BubbleNode node) { if (wouldKillSelf(node)) return; - final var found = nodeDAO.findByIp4(node.getIp4()); - if (found == null) { + final var nodeFromDB = nodeDAO.findByIp4(node.getIp4()); + if (nodeFromDB == null) { final String message = prefix() + "processNode: no node exists with ip4=" + node.getIp4() + ", killing it"; log.warn(message); reportError(message); @@ -78,15 +78,19 @@ public class NodeReaper extends SimpleDaemon { log.error(errMessage, e); } } else { - if (networkService.isReachable(node)) { - unreachableSince.remove(node.getUuid()); + if (networkService.isReachable(nodeFromDB)) { + unreachableSince.remove(nodeFromDB.getUuid()); } else { - final long downTime = unreachableSince.computeIfAbsent(node.getUuid(), k -> now()); - if (now() - downTime > MAX_DOWNTIME_BEFORE_DELETION) { - final String message = prefix() + "processNode: deleting node (" + node.id() + ") that has been down since " + TimeUtil.DATE_FORMAT_YYYY_MM_DD_HH_mm_ss.print(downTime); + final var downTime = unreachableSince.get(nodeFromDB.getUuid()); + if (downTime == null) { + unreachableSince.put(nodeFromDB.getUuid(), now()); + } else if (now() - downTime > MAX_DOWNTIME_BEFORE_DELETION) { + final var message = prefix() + "processNode: deleting node that has been down since " + + TimeUtil.DATE_FORMAT_YYYY_MM_DD_HH_mm_ss.print(downTime) + + " node=" + nodeFromDB.id(); log.warn(message); reportError(message); - nodeDAO.delete(node.getUuid()); + nodeDAO.delete(nodeFromDB.getUuid()); } } } diff --git a/bubble-server/src/main/java/bubble/dao/account/TrustedClientDAO.java b/bubble-server/src/main/java/bubble/dao/account/TrustedClientDAO.java index d5952988..22b11a9f 100644 --- a/bubble-server/src/main/java/bubble/dao/account/TrustedClientDAO.java +++ b/bubble-server/src/main/java/bubble/dao/account/TrustedClientDAO.java @@ -16,8 +16,8 @@ public class TrustedClientDAO extends AccountOwnedEntityDAO { return super.preCreate(trusted.setTrustId(randomUUID().toString())); } - @Override public TrustedClient postCreate(TrustedClient trusted, Object context) { - return super.postCreate(trusted, context); + public TrustedClient findByAccountAndDevice(String accountUuid, String deviceUuid) { + return findByUniqueFields("account", accountUuid, "device", deviceUuid); } } diff --git a/bubble-server/src/main/java/bubble/dao/device/DeviceDAO.java b/bubble-server/src/main/java/bubble/dao/device/DeviceDAO.java index 8d281305..3686a00c 100644 --- a/bubble-server/src/main/java/bubble/dao/device/DeviceDAO.java +++ b/bubble-server/src/main/java/bubble/dao/device/DeviceDAO.java @@ -67,7 +67,9 @@ public class DeviceDAO extends AccountOwnedEntityDAO { @Transactional @Override public Device create(@NonNull final Device device) { - if (isRawMode() || device.uninitialized()) return super.create(device); + if (isRawMode() || device.uninitialized() || device.getDeviceType().isNonVpnDevice()) { + return super.create(device); + } synchronized (createLock) { device.initDeviceType(); diff --git a/bubble-server/src/main/java/bubble/main/GenerateAlgoConfMain.java b/bubble-server/src/main/java/bubble/main/GenerateAlgoConfMain.java index 2db167c9..190251cc 100644 --- a/bubble-server/src/main/java/bubble/main/GenerateAlgoConfMain.java +++ b/bubble-server/src/main/java/bubble/main/GenerateAlgoConfMain.java @@ -51,7 +51,7 @@ public class GenerateAlgoConfMain extends BaseMain { private List loadDevices() { try { - final String sqlResult = execScript("echo \"select uuid from device where enabled = TRUE\" | PGPASSWORD=\"$(cat /home/bubble/.BUBBLE_PG_PASSWORD)\" psql -U bubble -h 127.0.0.1 bubble -qt"); + final String sqlResult = execScript("echo \"select uuid from device where enabled = TRUE and device_type != 'non_vpn'\" | PGPASSWORD=\"$(cat /home/bubble/.BUBBLE_PG_PASSWORD)\" psql -U bubble -h 127.0.0.1 bubble -qt"); final List deviceUuids = Arrays.stream(sqlResult.split("\n")) .filter(device -> !empty(device)) .map(String::trim) diff --git a/bubble-server/src/main/java/bubble/model/account/Account.java b/bubble-server/src/main/java/bubble/model/account/Account.java index 513521a2..de7b8b02 100644 --- a/bubble-server/src/main/java/bubble/model/account/Account.java +++ b/bubble-server/src/main/java/bubble/model/account/Account.java @@ -8,10 +8,7 @@ import bubble.dao.account.AccountInitializer; import bubble.model.app.AppData; import bubble.model.app.BubbleApp; import bubble.model.app.RuleDriver; -import bubble.model.bill.AccountPayment; -import bubble.model.bill.AccountPaymentMethod; -import bubble.model.bill.AccountPlan; -import bubble.model.bill.Bill; +import bubble.model.bill.*; import bubble.model.boot.ActivationRequest; import bubble.model.cloud.*; import bubble.model.cloud.notify.ReceivedNotification; @@ -82,10 +79,12 @@ import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; @Entity @NoArgsConstructor @Accessors(chain=true) @Slf4j public class Account extends IdentifiableBaseParentEntity implements TokenPrincipal, SqlViewSearchResult { - public static final String[] UPDATE_FIELDS = {"url", "description", "autoUpdatePolicy", "syncPassword"}; + public static final String[] UPDATE_FIELDS = { + "url", "description", "autoUpdatePolicy", "syncPassword", "preferredPlan" + }; public static final String[] ADMIN_UPDATE_FIELDS = ArrayUtil.append(UPDATE_FIELDS, "suspended", "admin"); public static final String[] CREATE_FIELDS = ArrayUtil.append(ADMIN_UPDATE_FIELDS, - "name", "termsAgreed", "preferredPlan"); + "name", "termsAgreed"); public static final String ROOT_USERNAME = "root"; public static final String ROOT_EMAIL = "root@local.local"; @@ -198,6 +197,7 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci } @Column(length=UUID_MAXLEN) + @ECForeignKey(entity=BubblePlan.class, index=false, cascade=false) @Getter @Setter private String preferredPlan; public boolean hasPreferredPlan () { return !empty(preferredPlan); } diff --git a/bubble-server/src/main/java/bubble/model/account/TrustedClient.java b/bubble-server/src/main/java/bubble/model/account/TrustedClient.java index 3a72f756..7afaed55 100644 --- a/bubble-server/src/main/java/bubble/model/account/TrustedClient.java +++ b/bubble-server/src/main/java/bubble/model/account/TrustedClient.java @@ -5,6 +5,7 @@ package bubble.model.account; +import bubble.model.device.Device; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; import lombok.NoArgsConstructor; @@ -25,7 +26,10 @@ import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENC_PAD; @Entity @ECType(root=true) @Slf4j @NoArgsConstructor @Accessors(chain=true) -@ECIndexes({@ECIndex(unique=true, of={"account", "trustId"})}) +@ECIndexes({ + @ECIndex(unique=true, of={"account", "trustId"}), + @ECIndex(unique=true, of={"account", "device"}) +}) public class TrustedClient extends IdentifiableBase implements HasAccount { @ECSearchable @ECField(index=10) @@ -33,6 +37,11 @@ public class TrustedClient extends IdentifiableBase implements HasAccount { @Column(nullable=false, updatable=false, length=UUID_MAXLEN) @Getter @Setter private String account; + @ECSearchable @ECField(index=20) + @ECForeignKey(entity=Device.class) + @Column(nullable=false, updatable=false, length=UUID_MAXLEN) + @Getter @Setter private String device; + @ECField(index=20) @Type(type=ENCRYPTED_STRING) @Column(updatable=false, columnDefinition="varchar("+(100+ENC_PAD)+") NOT NULL") @JsonIgnore @Getter @Setter private String trustId; diff --git a/bubble-server/src/main/java/bubble/model/account/TrustedClientLoginRequest.java b/bubble-server/src/main/java/bubble/model/account/TrustedClientLoginRequest.java index 4f84e4eb..79019cf4 100644 --- a/bubble-server/src/main/java/bubble/model/account/TrustedClientLoginRequest.java +++ b/bubble-server/src/main/java/bubble/model/account/TrustedClientLoginRequest.java @@ -31,6 +31,10 @@ public class TrustedClientLoginRequest { private String password; public boolean hasPassword () { return !empty(password); } + @HasValue(message="err.device.required") + @Getter @Setter private String device; + public boolean hasDevice () { return !empty(device); } + // require timestamp to begin with a '1'. // note: this means this pattern will break on October 11, 2603 private static final String TRUST_HASH_REGEX = "^1[\\d]{10}-"+UUID_REGEX+"-"+UUID_REGEX+"$"; diff --git a/bubble-server/src/main/java/bubble/model/bill/AccountPaymentMethod.java b/bubble-server/src/main/java/bubble/model/bill/AccountPaymentMethod.java index ff9b2e13..f693f092 100644 --- a/bubble-server/src/main/java/bubble/model/bill/AccountPaymentMethod.java +++ b/bubble-server/src/main/java/bubble/model/bill/AccountPaymentMethod.java @@ -6,6 +6,7 @@ package bubble.model.bill; import bubble.cloud.CloudServiceType; import bubble.cloud.payment.PaymentServiceDriver; +import bubble.dao.bill.BubblePlanDAO; import bubble.dao.cloud.CloudServiceDAO; import bubble.model.account.Account; import bubble.model.account.HasAccountNoName; @@ -48,7 +49,7 @@ public class AccountPaymentMethod extends IdentifiableBase implements HasAccount @Override public ScrubbableField[] fieldsToScrub() { return SCRUB_FIELDS; } - public static final String[] CREATE_FIELDS = {"paymentMethodType", "paymentInfo", "maskedPaymentInfo", "cloud"}; + public static final String[] CREATE_FIELDS = {"paymentMethodType", "paymentInfo", "maskedPaymentInfo", "cloud", "preferredPlan"}; public static final String[] VALIDATION_SET_FIELDS = {"paymentInfo", "maskedPaymentInfo"}; public AccountPaymentMethod(AccountPaymentMethod other) { copy(this, other, CREATE_FIELDS); } @@ -102,6 +103,9 @@ public class AccountPaymentMethod extends IdentifiableBase implements HasAccount @Transient @Getter @Setter private transient Boolean requireValidatedEmail = null; public boolean requireValidatedEmail() { return requireValidatedEmail == null || requireValidatedEmail; } + @Transient @Getter @Setter private transient String preferredPlan; + public boolean hasPreferredPlan() { return !empty(preferredPlan); } + public ValidationResult validate(ValidationResult result, BubbleConfiguration configuration) { if (!hasPaymentMethodType()) { @@ -143,7 +147,7 @@ public class AccountPaymentMethod extends IdentifiableBase implements HasAccount if (empty(getPaymentInfo())) { result.addViolation("err.paymentInfo.required"); } else { - log.info("validate: starting validation of payment method with this.requireValidatedEmail="+requireValidatedEmail); + log.debug("validate: starting validation of payment method with this.requireValidatedEmail="+requireValidatedEmail); final PaymentValidationResult validationResult = paymentDriver.validate(this); if (validationResult.hasErrors()) { result.addAll(validationResult.getViolations()); @@ -153,7 +157,15 @@ public class AccountPaymentMethod extends IdentifiableBase implements HasAccount } } } - + if (hasPreferredPlan()) { + final BubblePlanDAO planDAO = configuration.getBean(BubblePlanDAO.class); + final BubblePlan plan = planDAO.findById(preferredPlan); + if (plan == null) { + result.addViolation("err.plan.notFound"); + } else { + setPreferredPlan(plan.getUuid()); + } + } return result; } diff --git a/bubble-server/src/main/java/bubble/model/device/BubbleDeviceType.java b/bubble-server/src/main/java/bubble/model/device/BubbleDeviceType.java index aab542d4..b6af556e 100644 --- a/bubble-server/src/main/java/bubble/model/device/BubbleDeviceType.java +++ b/bubble-server/src/main/java/bubble/model/device/BubbleDeviceType.java @@ -25,6 +25,7 @@ public enum BubbleDeviceType { android (CertType.cer, true, DeviceSecurityLevel.basic), linux (CertType.crt, true, DeviceSecurityLevel.standard), firefox (CertType.crt, false), + web_client (null, false, DeviceSecurityLevel.disabled), other (null, true, DeviceSecurityLevel.basic); @Getter private final CertType certType; @@ -36,6 +37,9 @@ public enum BubbleDeviceType { @JsonCreator public static BubbleDeviceType fromString (String v) { return enumFromString(BubbleDeviceType.class, v); } + public boolean isNonVpnDevice () { return this == web_client; } + public boolean isVpnDevice () { return !isNonVpnDevice(); } + @Getter(lazy=true) private static final List selectableTypes = initSelectable(); private static List initSelectable() { return Arrays.stream(values()) diff --git a/bubble-server/src/main/java/bubble/model/device/Device.java b/bubble-server/src/main/java/bubble/model/device/Device.java index dbce006f..93fff6bc 100644 --- a/bubble-server/src/main/java/bubble/model/device/Device.java +++ b/bubble-server/src/main/java/bubble/model/device/Device.java @@ -4,7 +4,6 @@ */ package bubble.model.device; -import bubble.ApiConstants; import bubble.model.account.Account; import bubble.model.account.HasAccount; import bubble.model.cloud.BubbleNetwork; @@ -22,10 +21,10 @@ import org.hibernate.annotations.Type; import javax.persistence.*; import javax.validation.constraints.Size; - import java.io.File; import static bubble.ApiConstants.EP_DEVICES; +import static bubble.ApiConstants.HOME_DIR; import static bubble.model.device.BubbleDeviceType.other; import static bubble.model.device.BubbleDeviceType.uninitialized; import static java.util.UUID.randomUUID; @@ -51,10 +50,10 @@ public class Device extends IdentifiableBase implements HasAccount { public static final String UNINITIALIZED_DEVICE = "__uninitialized_device__"; public static final String UNINITIALIZED_DEVICE_LIKE = UNINITIALIZED_DEVICE+"%"; - public static final String VPN_CONFIG_PATH = ApiConstants.HOME_DIR + "/configs/localhost/wireguard/"; + public static final String VPN_CONFIG_PATH = HOME_DIR + "/configs/localhost/wireguard/"; - public File qrFile () { return new File(Device.VPN_CONFIG_PATH+getUuid()+".png"); } - public File vpnConfFile () { return new File(Device.VPN_CONFIG_PATH+getUuid()+".conf"); } + public File qrFile () { return new File(VPN_CONFIG_PATH+getUuid()+".png"); } + public File vpnConfFile () { return new File(VPN_CONFIG_PATH+getUuid()+".conf"); } public boolean configsOk () { return qrFile().exists() && vpnConfFile().exists(); } public Device (Device other) { copy(this, other, CREATE_FIELDS); } diff --git a/bubble-server/src/main/java/bubble/resources/account/TrustedAuthResource.java b/bubble-server/src/main/java/bubble/resources/account/TrustedAuthResource.java index 6c812126..d04347ea 100644 --- a/bubble-server/src/main/java/bubble/resources/account/TrustedAuthResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/TrustedAuthResource.java @@ -8,8 +8,10 @@ import bubble.dao.SessionDAO; import bubble.dao.account.AccountDAO; import bubble.dao.account.AccountPolicyDAO; import bubble.dao.account.TrustedClientDAO; +import bubble.dao.device.DeviceDAO; import bubble.model.account.*; import bubble.model.account.message.ActionTarget; +import bubble.model.device.Device; import bubble.service.account.StandardAuthenticatorService; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -21,7 +23,6 @@ import javax.validation.Valid; import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; -import java.util.List; import static bubble.ApiConstants.EP_DELETE; import static bubble.resources.account.AuthResource.newLoginSession; @@ -41,6 +42,7 @@ public class TrustedAuthResource { @Autowired private AccountDAO accountDAO; @Autowired private AccountPolicyDAO policyDAO; + @Autowired private DeviceDAO deviceDAO; @Autowired private SessionDAO sessionDAO; @Autowired private StandardAuthenticatorService authenticatorService; @Autowired private TrustedClientDAO trustedClientDAO; @@ -55,10 +57,20 @@ public class TrustedAuthResource { final Account account = validateAccountLogin(request.getEmail(), request.getPassword()); if (!account.getUuid().equals(caller.getUuid())) return notFound(request.getEmail()); + final Device device = deviceDAO.findByAccountAndId(account.getUuid(), request.getDevice()); + if (device == null) return notFound(request.getDevice()); + + // is there an existing trusted client for this device? + final TrustedClient existing = trustedClientDAO.findByAccountAndDevice(account.getUuid(), device.getUuid()); + if (existing != null) return invalid("err.device.alreadyTrusted"); + final AccountPolicy policy = policyDAO.findSingleByAccount(account.getUuid()); authenticatorService.ensureAuthenticated(ctx, policy, ActionTarget.account); - return ok(new TrustedClientResponse(trustedClientDAO.create(new TrustedClient().setAccount(account.getUuid())).getTrustId())); + final TrustedClient trusted = new TrustedClient() + .setAccount(account.getUuid()) + .setDevice(device.getUuid()); + return ok(new TrustedClientResponse(trustedClientDAO.create(trusted).getTrustId())); } @POST @@ -75,15 +87,17 @@ public class TrustedAuthResource { return ok(account.setToken(newLoginSession(account, accountDAO, sessionDAO))); } - @POST @Path(EP_DELETE) + @DELETE @Path(EP_DELETE+"/{device}") public Response removeTrustedClient(@Context ContainerRequest ctx, - @Valid TrustedClientLoginRequest request) { + @PathParam("device") String deviceId) { final Account caller = userPrincipal(ctx); - final Account validated = validateAccountLogin(request.getEmail(), request.getPassword()); - if (!validated.getUuid().equals(caller.getUuid())) return notFound(request.getEmail()); - final Account account = validateTrustedCall(request); - final TrustedClient trusted = findTrustedClient(account, request); + final Device device = deviceDAO.findByAccountAndId(caller.getUuid(), deviceId); + if (device == null) return notFound(deviceId); + + final TrustedClient trusted = trustedClientDAO.findByAccountAndDevice(caller.getUuid(), device.getUuid()); + if (trusted == null) return notFound(deviceId); + trustedClientDAO.delete(trusted.getUuid()); return ok_empty(); } @@ -116,9 +130,12 @@ public class TrustedAuthResource { } private TrustedClient findTrustedClient(Account account, TrustedClientLoginRequest request) { - final List trustedClients = trustedClientDAO.findByAccount(account.getUuid()); - final TrustedClient trusted = trustedClients.stream().filter(c -> c.isValid(request)).findFirst().orElse(null); + final TrustedClient trusted = trustedClientDAO.findByAccountAndDevice(account.getUuid(), request.getDevice()); if (trusted == null) { + log.warn("findTrustedClient: no TrustedClient found for device"); + throw notFoundEx(request.getDevice()); + } + if (!trusted.isValid(request)) { log.warn("findTrustedClient: no TrustedClient found for salt/hash"); throw notFoundEx(request.getTrustHash()); } diff --git a/bubble-server/src/main/java/bubble/resources/bill/AccountPaymentMethodsResource.java b/bubble-server/src/main/java/bubble/resources/bill/AccountPaymentMethodsResource.java index db4742d4..e893d021 100644 --- a/bubble-server/src/main/java/bubble/resources/bill/AccountPaymentMethodsResource.java +++ b/bubble-server/src/main/java/bubble/resources/bill/AccountPaymentMethodsResource.java @@ -75,4 +75,11 @@ public class AccountPaymentMethodsResource extends AccountOwnedResource