@@ -164,7 +164,7 @@ public class AccountDAO extends AbstractCRUDDAO<Account> implements SqlViewSearc | |||
// create an uninitialized device for the account, but only if this is a regular node network | |||
// sage networks do not allow devices, they launch and manage other regular node networks | |||
if (!account.isRoot() && !configuration.isSage()) { | |||
if (!isFirstAdmin(account) && !configuration.isSage()) { | |||
deviceDAO.ensureAllSpareDevices(account.getUuid(), configuration.getThisNetwork().getUuid()); | |||
deviceDAO.refreshVpnUsers(); | |||
} | |||
@@ -475,7 +475,8 @@ public class AccountDAO extends AbstractCRUDDAO<Account> implements SqlViewSearc | |||
private final Refreshable<Account> firstAdmin = new Refreshable<>("firstAdmin", FIRST_ADMIN_CACHE_MILLIS, this::findFirstAdmin); | |||
public Account getFirstAdmin() { return firstAdmin.get(); } | |||
public boolean isFirstAdmin(Account account) { return getFirstAdmin().getUuid().equals(account.getUuid()); } | |||
public boolean isFirstAdmin(Account account) { return isFirstAdmin(account.getUuid()); } | |||
public boolean isFirstAdmin(String accountUuid) { return getFirstAdmin().getUuid().equals(accountUuid); } | |||
public Account findFirstAdmin() { | |||
final List<Account> admins = findByField("admin", true); | |||
@@ -81,7 +81,7 @@ public class DeviceDAO extends AccountOwnedEntityDAO<Device> { | |||
var uninitializedDevices = findByAccountAndUninitialized(accountUuid); | |||
if (uninitializedDevices.size() <= SPARE_DEVICES_PER_ACCOUNT_THRESHOLD | |||
&& !configuration.getBean(AccountDAO.class).findByUuid(accountUuid).isRoot()) { | |||
&& !configuration.getBean(AccountDAO.class).isFirstAdmin(accountUuid)) { | |||
if (ensureAllSpareDevices(accountUuid, device.getNetwork())) refreshVpnUsers(); | |||
} | |||
@@ -112,7 +112,6 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci | |||
@Override @Transient public String getName() { return getEmail(); } | |||
public Account setName(String n) { return setEmail(n); } | |||
@Transient @JsonIgnore public boolean isRoot() { return getName().equals(ROOT_USERNAME); } | |||
// 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?) | |||
@@ -166,6 +166,9 @@ public class AccountPlan extends IdentifiableBase implements HasNetwork { | |||
@Transient @Getter @Setter private transient String forkHost = null; | |||
public boolean hasForkHost () { return !empty(forkHost); } | |||
@Transient @Getter @Setter private transient String adminEmail = null; | |||
public boolean hasAdminEmail() { return !empty(adminEmail); } | |||
@Transient @Getter @Setter private transient Boolean syncAccount = null; | |||
public boolean syncAccount() { return syncAccount == null || syncAccount; } | |||
@@ -200,7 +203,8 @@ public class AccountPlan extends IdentifiableBase implements HasNetwork { | |||
.setComputeSizeType(plan.getComputeSizeType()) | |||
.setStorage(storage.getUuid()) | |||
.setLaunchType(hasForkHost() && hasLaunchType() ? getLaunchType() : LaunchType.node) | |||
.setForkHost(hasForkHost() ? getForkHost() : null); | |||
.setForkHost(hasForkHost() ? getForkHost() : null) | |||
.setAdminEmail(hasAdminEmail() ? getAdminEmail() : null); | |||
} | |||
} |
@@ -23,7 +23,8 @@ import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
public class ActivationRequest { | |||
@HasValue(message="err.email.required") | |||
@Getter @Setter private String email; | |||
@Getter private String email; | |||
public ActivationRequest setEmail(String e) { this.email = empty(e) ? e : e.trim(); return this; } | |||
public boolean hasEmail() { return !empty(email); } | |||
public String getName() { return getEmail(); } | |||
@@ -55,6 +56,9 @@ public class ActivationRequest { | |||
@Getter @Setter private Boolean skipTests = false; | |||
public boolean skipTests () { return bool(skipTests); }; | |||
@Getter @Setter private Boolean skipPacker = false; | |||
public boolean skipPacker () { return bool(skipPacker); }; | |||
@Getter @Setter private AccountSshKey sshKey; | |||
public boolean hasSshKey () { return sshKey != null; } | |||
@@ -208,6 +208,9 @@ public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBu | |||
public boolean hasForkHost () { return !empty(forkHost); } | |||
public boolean fork() { return hasForkHost(); } | |||
@Transient @Getter @Setter private transient String adminEmail; | |||
public boolean hasAdminEmail () { return !empty(adminEmail); } | |||
@ECField(index=190) @Column(length=20, updatable=false) | |||
@Enumerated(EnumType.STRING) @Getter @Setter private LaunchType launchType = null; | |||
public boolean hasLaunchType () { return launchType != null; } | |||
@@ -5,13 +5,20 @@ | |||
package bubble.model.cloud; | |||
import com.fasterxml.jackson.annotation.JsonCreator; | |||
import lombok.AllArgsConstructor; | |||
import static bubble.ApiConstants.enumFromString; | |||
@AllArgsConstructor | |||
public enum LaunchType { | |||
node, fork_node, fork_sage; | |||
node (false), | |||
fork_node (true), | |||
fork_sage (true); | |||
@JsonCreator public static LaunchType fromString(String v) { return enumFromString(LaunchType.class, v); } | |||
private final boolean fork; | |||
public boolean fork () { return fork; } | |||
} |
@@ -166,6 +166,11 @@ public class AuthResource { | |||
if (accountDAO.activated()) { | |||
return invalid("err.activation.alreadyDone", "activation has already been done"); | |||
} | |||
if (!request.hasEmail()) return invalid("err.email.required", "email is required"); | |||
if (request.getEmail().contains("{{") && request.getEmail().contains("}}")) { | |||
request.setEmail(configuration.applyHandlebars(request.getEmail()).trim()); | |||
} | |||
if (!request.hasEmail()) return invalid("err.email.required", "email is required"); | |||
if (!request.hasPassword()) return invalid("err.password.required", "password is required"); | |||
@@ -41,10 +41,10 @@ import java.util.List; | |||
import java.util.stream.Collectors; | |||
import static bubble.ApiConstants.*; | |||
import static bubble.model.account.Account.ROOT_EMAIL; | |||
import static bubble.model.cloud.BubbleNetwork.validateHostname; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | |||
import static org.cobbzilla.util.string.ValidationRegexes.HOST_PATTERN; | |||
import static org.cobbzilla.util.string.ValidationRegexes.validateRegexMatches; | |||
import static org.cobbzilla.util.string.ValidationRegexes.*; | |||
import static org.cobbzilla.wizard.model.NamedEntity.NAME_MAXLEN; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||
@@ -167,6 +167,18 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco | |||
validateName(request, errors); | |||
} | |||
} | |||
final String adminEmail = request.getAdminEmail(); | |||
if (request.hasLaunchType() && request.getLaunchType().fork() && caller.admin()) { | |||
if (empty(adminEmail) && caller.getEmail().equals(ROOT_EMAIL)) { | |||
errors.addViolation("err.adminEmail.required"); | |||
} else if (!empty(adminEmail) && !validateRegexMatches(EMAIL_PATTERN, adminEmail)) { | |||
errors.addViolation("err.adminEmail.invalid"); | |||
} | |||
} else { | |||
if (!empty(adminEmail)) { | |||
errors.addViolation("err.adminEmail.cannotSet"); | |||
} | |||
} | |||
} else { | |||
if (!request.hasNickname()) { | |||
if (!request.hasName()) request.setName(newNetworkName()); | |||
@@ -28,8 +28,8 @@ public class ComputePackerResource { | |||
@Autowired private BubbleConfiguration configuration; | |||
@Autowired private PackerService packer; | |||
private Account account; | |||
private CloudService cloud; | |||
private final Account account; | |||
private final CloudService cloud; | |||
public ComputePackerResource (Account account, CloudService cloud) { | |||
this.account = account; | |||
@@ -18,6 +18,7 @@ import bubble.model.boot.ActivationRequest; | |||
import bubble.model.boot.CloudServiceConfig; | |||
import bubble.model.cloud.*; | |||
import bubble.server.BubbleConfiguration; | |||
import bubble.service.packer.PackerService; | |||
import com.fasterxml.jackson.databind.JsonNode; | |||
import lombok.Cleanup; | |||
import lombok.Getter; | |||
@@ -64,6 +65,7 @@ public class ActivationService { | |||
@Autowired private StandardSelfNodeService selfNodeService; | |||
@Autowired private BubbleConfiguration configuration; | |||
@Autowired private ModelSetupService modelSetupService; | |||
@Autowired private PackerService packerService; | |||
public BubbleNode bootstrapThisNode(Account account, ActivationRequest request) { | |||
String ip = getFirstPublicIpv4(); | |||
@@ -228,12 +230,26 @@ public class ActivationService { | |||
final Map<CrudOperation, Collection<Identifiable>> objects | |||
= modelSetupService.setupModel(api, account, "manifest-defaults"); | |||
log.info("bootstrapThisNode: created default objects\n"+json(objects)); | |||
if (!request.skipPacker()) initialPacker(account); | |||
}, "ActivationService.bootstrapThisNode.createDefaultObjects"); | |||
} else if (!request.skipPacker()) { | |||
initialPacker(account); | |||
} | |||
return node; | |||
} | |||
private void initialPacker(Account account) { | |||
for (CloudService cloud : cloudDAO.findByAccountAndType(account.getUuid(), CloudServiceType.compute)) { | |||
log.info("initialPacker: creating images for compute cloud: "+cloud.getName()); | |||
packerService.writePackerImages(cloud, AnsibleInstallType.sage, null); | |||
packerService.writePackerImages(cloud, AnsibleInstallType.node, null); | |||
} | |||
} | |||
public BubbleNetwork createRootNetwork(BubbleNetwork network) { | |||
network.setUuid(ROOT_NETWORK_UUID); | |||
return networkDAO.create(network); | |||
@@ -15,10 +15,12 @@ import bubble.dao.cloud.BubbleNodeDAO; | |||
import bubble.dao.cloud.BubbleNodeKeyDAO; | |||
import bubble.dao.cloud.CloudServiceDAO; | |||
import bubble.dao.device.DeviceDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.bill.AccountPlan; | |||
import bubble.model.bill.BubblePlan; | |||
import bubble.model.cloud.*; | |||
import bubble.model.cloud.BubbleNetwork; | |||
import bubble.model.cloud.BubbleNode; | |||
import bubble.model.cloud.BubbleNodeKey; | |||
import bubble.model.cloud.BubbleNodeState; | |||
import bubble.model.cloud.notify.NotificationReceipt; | |||
import bubble.model.cloud.notify.NotificationType; | |||
import bubble.server.BubbleConfiguration; | |||
@@ -29,7 +31,6 @@ import lombok.Getter; | |||
import lombok.NonNull; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.util.cache.Refreshable; | |||
import org.cobbzilla.util.daemon.SimpleDaemon; | |||
import org.cobbzilla.util.http.HttpSchemes; | |||
import org.cobbzilla.util.http.HttpUtil; | |||
import org.cobbzilla.util.io.FileUtil; | |||
@@ -53,7 +54,6 @@ import static bubble.server.BubbleServer.disableRestoreMode; | |||
import static bubble.server.BubbleServer.isRestoreMode; | |||
import static java.util.concurrent.TimeUnit.DAYS; | |||
import static java.util.concurrent.TimeUnit.MINUTES; | |||
import static java.util.function.Predicate.not; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | |||
import static org.cobbzilla.util.io.FileUtil.abs; | |||
import static org.cobbzilla.util.io.FileUtil.toFileOrDie; | |||
@@ -162,7 +162,7 @@ public class StandardSelfNodeService implements SelfNodeService { | |||
background(() -> { | |||
if (accountDAO.findAll() | |||
.stream() | |||
.filter(not(Account::isRoot)) | |||
.filter(a -> !accountDAO.isFirstAdmin(a)) | |||
.map(a -> deviceDAO.ensureAllSpareDevices(a.getUuid(), thisNetworkUuid)) | |||
.reduce(false, Boolean::logicalOr) | |||
.booleanValue()) { | |||
@@ -7,9 +7,7 @@ package bubble.service.dbfilter; | |||
import bubble.cloud.CloudServiceType; | |||
import bubble.cloud.storage.local.LocalStorageConfig; | |||
import bubble.cloud.storage.local.LocalStorageDriver; | |||
import bubble.model.account.Account; | |||
import bubble.model.account.AccountSshKey; | |||
import bubble.model.account.AccountTemplate; | |||
import bubble.model.account.*; | |||
import bubble.model.app.AppTemplateEntity; | |||
import bubble.model.app.BubbleApp; | |||
import bubble.model.bill.AccountPaymentMethod; | |||
@@ -135,7 +133,29 @@ public abstract class EntityIterator implements Iterator<Identifiable> { | |||
.forEach(this::add); | |||
} else if (Account.class.isAssignableFrom(c)) { | |||
entities.forEach(e -> add(((Account) e).setPreferredPlan(null))); | |||
entities.forEach(e -> { | |||
if (network.hasAdminEmail() && network.getAccount().equals(e.getUuid())) { | |||
final Account a = (Account) e; | |||
a.setEmail(network.getAdminEmail()); | |||
} | |||
add(((Account) e).setPreferredPlan(null)); | |||
}); | |||
} else if (AccountPolicy.class.isAssignableFrom(c) && network.hasAdminEmail()) { | |||
entities.forEach(e -> { | |||
if (network.hasAdminEmail()) { | |||
final AccountPolicy p = (AccountPolicy) e; | |||
if (p.getAccount().equals(network.getAccount())) { | |||
final AccountContact adminContact = new AccountContact() | |||
.setType(CloudServiceType.email) | |||
.setInfo(network.getAdminEmail()) | |||
.setVerified(true) | |||
.setRemovable(false); | |||
p.setAccountContacts(new AccountContact[]{adminContact}); | |||
} | |||
} | |||
add(e); | |||
}); | |||
} else if (AccountSshKey.class.isAssignableFrom(c)) { | |||
entities.forEach(e -> add(setInstallKey((AccountSshKey) e, network))); | |||
@@ -1 +1 @@ | |||
Subproject commit 6557ae47aae789ce6dc6ff1aca189787a89103bb | |||
Subproject commit c0070a05666df680157c6740879ad5a3c98c3bba |
@@ -1 +1 @@ | |||
Subproject commit b7120e4099a382278c43813644f99e928c9ae828 | |||
Subproject commit 290017992084da8d799aba24409099d54c1a9305 |
@@ -54,9 +54,9 @@ | |||
} | |||
}, | |||
// You must configure the an SMTP service, it is required to send emails | |||
// SendGrid, MailGun, or any other SMTP service should work fine | |||
"SmtpServer": { | |||
// You must configure an email service, it is required to send emails | |||
// Comment out the email clouds that you don't use | |||
"SmtpEmail": { | |||
"config": { | |||
"tlsEnabled": true | |||
}, | |||
@@ -68,6 +68,21 @@ | |||
} | |||
}, | |||
"SendGridEmail": { | |||
"config": {}, | |||
"credentials": { | |||
"apiKey": "your_sendgrid_api_key" | |||
} | |||
}, | |||
"MailgunEmail": { | |||
"config": {}, | |||
"credentials": { | |||
"domain": "your_mailgun_domain", | |||
"apiKey": "your_mailgun_api_key" | |||
} | |||
}, | |||
// Required for TOTP-based authentication. Nothing to configure, just leave this as-is | |||
"TOTPAuthenticator": {}, | |||
@@ -1 +1 @@ | |||
Subproject commit d7ea904f35401302cbfb4bde5d70475d2721df07 | |||
Subproject commit d3b5e5254c2eca16290dc5746d0ab489160a8ff1 |