@@ -4,6 +4,7 @@ | |||
*/ | |||
package bubble.dao.cloud; | |||
import bubble.dao.account.AccountDAO; | |||
import bubble.dao.account.AccountOwnedEntityDAO; | |||
import bubble.dao.bill.AccountPlanDAO; | |||
import bubble.model.bill.AccountPlan; | |||
@@ -29,6 +30,7 @@ import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; | |||
@Repository @Slf4j | |||
public class BubbleNetworkDAO extends AccountOwnedEntityDAO<BubbleNetwork> { | |||
@Autowired private AccountDAO accountDAO; | |||
@Autowired private BubbleDomainDAO domainDAO; | |||
@Autowired private NetworkService networkService; | |||
@Autowired private BubbleBackupDAO backupDAO; | |||
@@ -40,7 +42,7 @@ public class BubbleNetworkDAO extends AccountOwnedEntityDAO<BubbleNetwork> { | |||
@Override public Object preCreate(BubbleNetwork network) { | |||
if (!network.hasForkHost()) { | |||
final ValidationResult errors = validateHostname(network); | |||
final ValidationResult errors = validateHostname(network, accountDAO, this); | |||
if (errors.isInvalid()) throw invalidEx(errors); | |||
} | |||
final AnsibleInstallType installType = network.hasForkHost() && configuration.isSageLauncher() | |||
@@ -85,6 +87,10 @@ public class BubbleNetworkDAO extends AccountOwnedEntityDAO<BubbleNetwork> { | |||
return findByUniqueFields("name", name, "domainName", domainName); | |||
} | |||
public BubbleNetwork findByNameAndDomainUuid(String name, String domainUuid) { | |||
return findByUniqueFields("name", name, "domain", domainUuid); | |||
} | |||
@Override public void delete(String uuid) { delete(uuid, false); } | |||
@Override public void forceDelete(String uuid) { delete(uuid, true); } | |||
@@ -116,9 +116,11 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci | |||
private static final List<String> RESERVED_NAMES = Arrays.asList( | |||
"root", "postmaster", "hostmaster", "webmaster", | |||
"ftp", "www", "www-data", "postgres", "ipfs", | |||
"dns", "dnscrypt", "dnscrypt-proxy", "ftp", "www", "www-data", "postgres", "ipfs", | |||
"redis", "nginx", "mitmproxy", "mitmdump", "algo", "algovpn"); | |||
public boolean hasReservedName () { return hasName() && RESERVED_NAMES.contains(getName()); } | |||
public boolean hasReservedName () { return hasName() && isReservedName(getName()); } | |||
public static boolean isReservedName(String name) { return RESERVED_NAMES.contains(name); } | |||
// make this updatable if we ever want accounts to be able to change parents | |||
// there might be a lot more involved in that action though (read-only parent objects that will no longer be visible, must be copied in?) | |||
@@ -6,7 +6,7 @@ package bubble.model.bill; | |||
import bubble.model.account.Account; | |||
import bubble.model.account.AccountSshKey; | |||
import bubble.model.account.HasAccount; | |||
import bubble.model.account.HasNetwork; | |||
import bubble.model.cloud.BubbleDomain; | |||
import bubble.model.cloud.BubbleNetwork; | |||
import bubble.model.cloud.CloudService; | |||
@@ -40,7 +40,7 @@ import static org.cobbzilla.util.reflect.ReflectionUtil.copy; | |||
@ECIndex(unique=true, of={"account", "network"}), | |||
@ECIndex(unique=true, of={"plan", "network"}) | |||
}) | |||
public class AccountPlan extends IdentifiableBase implements HasAccount { | |||
public class AccountPlan extends IdentifiableBase implements HasNetwork { | |||
public static final String[] UPDATE_FIELDS = {"description", "paymentMethod", "paymentMethodObject"}; | |||
@@ -5,6 +5,8 @@ | |||
package bubble.model.cloud; | |||
import bubble.cloud.compute.ComputeNodeSizeType; | |||
import bubble.dao.account.AccountDAO; | |||
import bubble.dao.cloud.BubbleNetworkDAO; | |||
import bubble.model.BubbleTags; | |||
import bubble.model.HasBubbleTags; | |||
import bubble.model.account.Account; | |||
@@ -20,7 +22,6 @@ import lombok.experimental.Accessors; | |||
import org.cobbzilla.util.collection.ArrayUtil; | |||
import org.cobbzilla.wizard.model.Identifiable; | |||
import org.cobbzilla.wizard.model.IdentifiableBase; | |||
import org.cobbzilla.wizard.model.NamedEntity; | |||
import org.cobbzilla.wizard.model.entityconfig.EntityFieldType; | |||
import org.cobbzilla.wizard.model.entityconfig.annotations.*; | |||
import org.cobbzilla.wizard.validation.HasValue; | |||
@@ -179,19 +180,36 @@ public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBu | |||
return die("hostFromFqdn("+fqdn+"): expected suffix ."+getNetworkDomain()); | |||
} | |||
public static ValidationResult validateHostname(NamedEntity request) { | |||
return validateHostname(request, new ValidationResult()); | |||
public static ValidationResult validateHostname(HasNetwork request, | |||
AccountDAO accountDAO, | |||
BubbleNetworkDAO networkDAO) { | |||
return validateHostname(request, new ValidationResult(), accountDAO, networkDAO); | |||
} | |||
public static ValidationResult validateHostname(NamedEntity request, ValidationResult errors) { | |||
public static ValidationResult validateHostname(HasNetwork request, | |||
ValidationResult errors, | |||
AccountDAO accountDAO, | |||
BubbleNetworkDAO networkDAO) { | |||
if (!request.hasName()) { | |||
errors.addViolation("err.name.required"); | |||
} else if (!validateRegexMatches(HOST_PART_PATTERN, request.getName())) { | |||
errors.addViolation("err.name.invalid"); | |||
} else if (request.getName().length() > NETWORK_NAME_MAXLEN) { | |||
errors.addViolation("err.name.length"); | |||
} else if (request.getName().length() < NETWORK_NAME_MINLEN) { | |||
errors.addViolation("err.name.tooShort"); | |||
} else { | |||
final String name = request.getName(); | |||
if (!validateRegexMatches(HOST_PART_PATTERN, name)) { | |||
errors.addViolation("err.name.invalid"); | |||
} else if (Account.isReservedName(name)) { | |||
errors.addViolation("err.name.reserved"); | |||
} else if (name.length() > NETWORK_NAME_MAXLEN) { | |||
errors.addViolation("err.name.length"); | |||
} else if (name.length() < NETWORK_NAME_MINLEN) { | |||
errors.addViolation("err.name.tooShort"); | |||
} else if (networkDAO.findByNameAndDomainUuid(name, request.getDomain()) != null) { | |||
errors.addViolation("err.name.alreadyInUse"); | |||
} else { | |||
final Account acct = accountDAO.findByName(name); | |||
if (acct != null && !acct.getUuid().equals(request.getAccount())) { | |||
errors.addViolation("err.name.reservedForAccount"); | |||
} | |||
} | |||
} | |||
return errors; | |||
} | |||
@@ -146,11 +146,11 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco | |||
errors.addViolation("err.forkHost.domainMismatch"); | |||
} else if (domain != null) { | |||
request.setName(domain.networkFromFqdn(forkHost, errors)); | |||
validateHostname(request, errors); | |||
validateHostname(request, errors, accountDAO, networkDAO); | |||
} | |||
} | |||
} else { | |||
validateHostname(request, errors); | |||
validateHostname(request, errors, accountDAO, networkDAO); | |||
} | |||
final BubblePlan plan = planDAO.findByAccountOrParentAndId(caller, request.getPlan()); | |||
@@ -199,9 +199,6 @@ field_label_network_ssh_key=SSH Key | |||
field_description_network_ssh_key=You can SSH into the Bubble with this key | |||
message_network_ssh_key_do_not_install=Do not install any SSH key | |||
field_label_send_metrics=Send error reports and usage statistics | |||
field_label_newPaymentMethod=Add a Payment Method | |||
field_label_existingPaymentMethod=Payment Method | |||
err_noPaymentMethods=No payment methods are configured. Contact the administrator of this system. | |||
msg_km_distance_away=km away | |||
message_auto_detecting=Auto-Detecting... <small>(refresh page if this gets stuck)</small> | |||
button_label_customize=Customize | |||
@@ -597,19 +594,11 @@ err.mitm.changeInProgress=A change to MITM is already in progress | |||
err.mitm.errorReadingControlFile=Error reading MITM control file | |||
err.mitm.errorWritingControlFile=Error writing MITM control file | |||
err.mitm.notInstalled=MITM component is not installed | |||
err.name.invalid=Name is invalid | |||
err.name.networkNameAlreadyExists=Name is already in use | |||
err.name.regexFailed=Name must start with a letter and can only contain letters, numbers, hyphens, periods and underscores | |||
err.node.name.alreadyExists=A node already exists with the same FQDN | |||
err.node.cannotStopLocalNode=Cannot stop local Bubble | |||
err.nodeOperationTimeout.required=Node operation timeout is required | |||
err.nodeOperationTimeout.tooLong=Node operation timeout cannot be longer than 3 days | |||
err.nodeOperationTimeout.tooShort=Node operation timeout cannot be shorter than 1 minute | |||
err.name.length=Name is too long | |||
err.name.tooShort=Name is too short | |||
err.name.mismatch=Name mismatch | |||
err.name.reserved=Name is reserved | |||
err.name.alreadyInUse=Name is already in use | |||
err.netlocation.invalid=Must specify both cloud and region, or neither | |||
err.network.alreadyStarted=Network is already started | |||
err.network.exists=A plan already exists for this network | |||
@@ -165,9 +165,17 @@ err.geoLocateService.notFound=GeoLocation service not found | |||
err.app.notFound=App not found | |||
err.name.required=Name is required | |||
err.name.notFound=Account not found | |||
err.name.length=Name is too long | |||
err.name.tooShort=Name must be at least 4 characters | |||
err.name.tooLong=Name cannot be longer than 100 characters | |||
err.name.planMaxAccountLimit=No more accounts can be created on this plan. Upgrade your plan or delete some accounts. | |||
err.name.alreadyInUse=Name is already in use | |||
err.name.reservedForAccount=Name is already in use | |||
err.name.reserved=Name is reserved | |||
err.name.invalid=Name is invalid | |||
err.name.networkNameAlreadyExists=Name is already in use | |||
err.name.regexFailed=Name must start with a letter and can only contain letters, numbers, hyphens, periods and underscores | |||
err.name.mismatch=Name mismatch | |||
err.password.required=Password is required | |||
err.password.tooShort=Password must be at least 8 characters | |||
err.password.invalid=Password must contain at least one letter, one number, and one special character | |||
@@ -266,6 +274,10 @@ message_login_agreeToTerms=By logging in, you agree to the <a target="_blank" re | |||
message_login_authenticator_auth=Login requires Authenticator code | |||
field_label_receiveInformationalMessages=Receive informational messages about your Bubble | |||
field_label_receivePromotionalMessages=Receive news about Bubble, including new releases and new features | |||
field_label_paymentMethod=Payment Method | |||
field_label_newPaymentMethod=Add a Payment Method | |||
field_label_existingPaymentMethod=Payment Method | |||
err_noPaymentMethods=No payment methods are configured. Contact the administrator of this system. | |||
button_label_login=Login | |||
button_label_register=Register | |||
button_label_forgotPassword=Forgot Password | |||