Browse Source

encrypt network restore keys, add ui support for stop/delete/restore bubble

tags/v0.1.6
Jonathan Cobb 4 years ago
parent
commit
f9ff2565a2
16 changed files with 191 additions and 63 deletions
  1. +1
    -0
      bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java
  2. +1
    -1
      bubble-server/src/main/java/bubble/dao/cloud/CloudServiceDAO.java
  3. +7
    -2
      bubble-server/src/main/java/bubble/model/account/AccountContact.java
  4. +6
    -0
      bubble-server/src/main/java/bubble/model/account/AccountPolicy.java
  5. +1
    -1
      bubble-server/src/main/java/bubble/model/cloud/BubbleNetworkState.java
  6. +1
    -0
      bubble-server/src/main/java/bubble/model/cloud/BubbleNode.java
  7. +21
    -3
      bubble-server/src/main/java/bubble/model/cloud/NetworkKeys.java
  8. +10
    -3
      bubble-server/src/main/java/bubble/resources/account/AuthResource.java
  9. +14
    -3
      bubble-server/src/main/java/bubble/resources/cloud/NetworkActionsResource.java
  10. +51
    -27
      bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java
  11. +23
    -8
      bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties
  12. +5
    -3
      bubble-server/src/test/resources/models/include/get_network_keys.json
  13. +6
    -2
      bubble-server/src/test/resources/models/tests/live/backup_and_restore.json
  14. +42
    -8
      bubble-server/src/test/resources/models/tests/network/network_keys.json
  15. +1
    -1
      bubble-web
  16. +1
    -1
      utils/cobbzilla-utils

+ 1
- 0
bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java View File

@@ -130,6 +130,7 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> {
throw invalidEx("err.accountPlan.stopNetworkBeforeDeleting");
}
update(accountPlan.setDeleted(now()).setEnabled(false));
networkDAO.delete(accountPlan.getNetwork());
if (configuration.paymentsEnabled()) {
refundService.processRefunds();
}


+ 1
- 1
bubble-server/src/main/java/bubble/dao/cloud/CloudServiceDAO.java View File

@@ -41,7 +41,7 @@ public class CloudServiceDAO extends AccountOwnedTemplateDAO<CloudService> {
}

@Override public CloudService postCreate(CloudService cloud, Object context) {
if (!cloud.delegated()) {
if (!cloud.delegated() && !configuration.testMode()) {
final ValidationResult errors = testDriver(cloud, configuration);
if (errors.isInvalid()) throw invalidEx(errors);
}


+ 7
- 2
bubble-server/src/main/java/bubble/model/account/AccountContact.java View File

@@ -19,8 +19,10 @@ import org.cobbzilla.wizard.validation.ValidationResult;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static bubble.ApiConstants.G_AUTH;
import static java.util.UUID.randomUUID;
@@ -284,8 +286,11 @@ public class AccountContact implements Serializable {
}

public AccountContact mask() {
return new AccountContact(this)
.setInfo(getType().mask(getInfo()));
return new AccountContact(this).setInfo(getType().mask(getInfo()));
}

public static Collection<AccountContact> mask(Collection<AccountContact> contacts) {
return empty(contacts) ? contacts : contacts.stream().map(c -> c.mask()).collect(Collectors.toList());
}

public ValidationResult validate(ValidationResult errors) {


+ 6
- 0
bubble-server/src/main/java/bubble/model/account/AccountPolicy.java View File

@@ -76,6 +76,12 @@ public class AccountPolicy extends IdentifiableBase implements HasAccount {
@Type(type=ENCRYPTED_STRING) @Column(columnDefinition="varchar("+(100000+ENC_PAD)+")")
@JsonIgnore @Getter @Setter private String accountContactsJson;
public boolean hasAccountContacts() { return accountContactsJson != null; }

@JsonIgnore @Transient public List<AccountContact> getVerifiedContacts () {
return hasVerifiedAccountContacts()
? Arrays.stream(getAccountContacts()).filter(AccountContact::verified).collect(Collectors.toList())
: Collections.emptyList();
}
public boolean hasVerifiedAccountContacts() {
return hasAccountContacts() && Arrays.stream(getAccountContacts()).anyMatch(AccountContact::verified);
}


+ 1
- 1
bubble-server/src/main/java/bubble/model/cloud/BubbleNetworkState.java View File

@@ -6,7 +6,7 @@ import static bubble.ApiConstants.enumFromString;

public enum BubbleNetworkState {

created, starting, restoring, running, stopping, stopped;
created, starting, restoring, running, stopping, error_stopping, stopped;

@JsonCreator public static BubbleNetworkState fromString(String v) { return enumFromString(BubbleNetworkState.class, v); }



+ 1
- 0
bubble-server/src/main/java/bubble/model/cloud/BubbleNode.java View File

@@ -145,6 +145,7 @@ public class BubbleNode extends IdentifiableBase implements HasNetwork, HasBubbl
@Enumerated(EnumType.STRING)
@ECIndex @Column(length=20, nullable=false)
@Getter @Setter private ComputeNodeSizeType sizeType;
@Transient @JsonIgnore public boolean isLocalCompute () { return sizeType == ComputeNodeSizeType.local; }

@ECSearchable @ECField(index=110)
@Column(nullable=false)


+ 21
- 3
bubble-server/src/main/java/bubble/model/cloud/NetworkKeys.java View File

@@ -6,6 +6,10 @@ import lombok.Setter;
import lombok.experimental.Accessors;
import org.cobbzilla.util.collection.NameAndValue;

import static org.cobbzilla.util.json.JsonUtil.json;
import static org.cobbzilla.util.security.CryptoUtil.string_decrypt;
import static org.cobbzilla.util.security.CryptoUtil.string_encrypt;

@NoArgsConstructor @Accessors(chain=true)
public class NetworkKeys {

@@ -14,8 +18,22 @@ public class NetworkKeys {

@Getter @Setter private NameAndValue[] keys;

public NetworkKeys addKey (String name, String value) {
return setKeys(NameAndValue.update(keys, name, value));
}
public NetworkKeys addKey (String name, String value) { return setKeys(NameAndValue.update(keys, name, value)); }

public EncryptedNetworkKeys encrypt(String password) { return new EncryptedNetworkKeys(this, password); }

@NoArgsConstructor @Accessors(chain=true)
public static class EncryptedNetworkKeys {

public EncryptedNetworkKeys (NetworkKeys keys, String password) {
setData(string_encrypt(json(keys), password));
}

public NetworkKeys decrypt () {
return json(string_decrypt(getData(), getPassword()), NetworkKeys.class);
}

@Getter @Setter private String data;
@Getter @Setter private String password;
}
}

+ 10
- 3
bubble-server/src/main/java/bubble/resources/account/AuthResource.java View File

@@ -46,8 +46,7 @@ import static bubble.model.cloud.BubbleNetwork.TAG_PARENT_ACCOUNT;
import static bubble.model.cloud.notify.NotificationType.retrieve_backup;
import static bubble.server.BubbleServer.getRestoreKey;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.util.daemon.ZillaRuntime.now;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON;
import static org.cobbzilla.util.http.HttpContentTypes.CONTENT_TYPE_ANY;
import static org.cobbzilla.util.system.Sleep.sleep;
@@ -117,7 +116,7 @@ public class AuthResource {
public Response restore(@Context Request req,
@Context ContainerRequest ctx,
@PathParam("restoreKey") String restoreKey,
@Valid NetworkKeys keys) {
@Valid NetworkKeys.EncryptedNetworkKeys encryptedKeys) {

// ensure we have been initialized
long start = now();
@@ -137,6 +136,14 @@ public class AuthResource {
final BubbleNode sageNode = nodeDAO.findByUuid(thisNode.getSageNode());
if (sageNode == null) return invalid("err.sageNode.notFound");

final NetworkKeys keys;
try {
keys = encryptedKeys.decrypt();
} catch (Exception e) {
log.warn("restore: error decrypting keys: "+shortError(e));
return invalid("err.networkKeys.invalid");
}

restoreService.registerRestore(restoreKey, keys);
final NotificationReceipt receipt = notificationService.notify(thisNode.getUuid(), sageNode, retrieve_backup, thisNode.setRestoreKey(getRestoreKey()));



+ 14
- 3
bubble-server/src/main/java/bubble/resources/cloud/NetworkActionsResource.java View File

@@ -19,6 +19,8 @@ import bubble.service.backup.NetworkKeysService;
import bubble.service.cloud.NodeProgressMeterTick;
import bubble.service.cloud.StandardNetworkService;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.collection.NameAndValue;
import org.cobbzilla.wizard.validation.ConstraintViolationBean;
import org.glassfish.grizzly.http.server.Request;
import org.glassfish.jersey.server.ContainerRequest;
import org.springframework.beans.factory.annotation.Autowired;
@@ -29,6 +31,7 @@ import javax.ws.rs.core.Response;
import java.util.List;

import static bubble.ApiConstants.*;
import static bubble.model.account.Account.validatePassword;
import static bubble.model.cloud.BubbleNetwork.TAG_ALLOW_REGISTRATION;
import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON;
import static org.cobbzilla.wizard.resources.ResourceUtil.*;
@@ -115,14 +118,22 @@ public class NetworkActionsResource {
return ok();
}

@GET @Path(EP_KEYS+"/{uuid}")
@POST @Path(EP_KEYS+"/{uuid}")
public Response retrieveNetworkKeys(@Context Request req,
@Context ContainerRequest ctx,
@PathParam("uuid") String uuid) {
@PathParam("uuid") String uuid,
NameAndValue enc) {
final Account caller = userPrincipal(ctx);
if (!caller.admin()) return forbidden();

final String encryptionKey = enc == null ? null : enc.getValue();
final ConstraintViolationBean error = validatePassword(encryptionKey);
if (error != null) return invalid(error);

final NetworkKeys keys = keysService.retrieveKeys(uuid);
return keys == null ? notFound(uuid) : ok(keys);
return keys == null
? invalid("err.retrieveNetworkKeys.notFound")
: ok(keys.encrypt(encryptionKey));
}

@POST @Path(EP_STOP)


+ 51
- 27
bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java View File

@@ -608,44 +608,68 @@ public class StandardNetworkService implements NetworkService {

public boolean stopNetwork(BubbleNetwork network) {
log.info("stopNetwork: stopping "+network.getNetworkDomain());

String lock = null;
final String networkUuid = network.getUuid();
boolean stopped = false;
try {
lock = lockNetwork(networkUuid);

network = networkDAO.findByUuid(networkUuid);
if (network == null) throw notFoundEx(networkUuid);
network = networkDAO.findByUuid(networkUuid);
if (network == null) throw notFoundEx(networkUuid);

// are any of them still alive?
final List<BubbleNode> nodes = nodeDAO.findByNetwork(network.getUuid());
if (nodes.isEmpty()) {
// nothing is running... what do we need to stop?
log.warn("stopNetwork: no nodes running");
}
// are any of them still alive?
final List<BubbleNode> nodes = nodeDAO.findByNetwork(networkUuid);
if (nodes.isEmpty()) {
// nothing is running... what do we need to stop?
log.warn("stopNetwork: no nodes running");
}

if (nodes.size() == 1) {
final BubbleNode n = nodes.get(0);
if (n.isLocalCompute()) {
throw invalidEx("err.node.cannotStopLocalNode", "Cannot stop local node: " + n.id(), n.id());
}
}

network.setState(BubbleNetworkState.stopping);
networkDAO.update(network);

network.setState(BubbleNetworkState.stopping);
networkDAO.update(network);
final ValidationResult validationResult = new ValidationResult();

final ValidationResult validationResult = new ValidationResult();
// todo: parallel shutdown?
// stop all nodes in network
nodes.forEach(node -> {
try {
stopNode(node);
log.info("stopNetwork: stopped node " + node.id());
} catch (Exception e) {
validationResult.addViolation("err.node.shutdownFailed", "Node shutdown failed: " + node.getUuid() + "/" + node.getIp4() + ": " + e);
}
});

// todo: parallel shutdown?
// stop all nodes in network
nodes.forEach(node -> {
try {
stopNode(node);
log.info("stopNetwork: stopped node "+node.id());
} catch (Exception e) {
validationResult.addViolation("err.node.shutdownFailed", "Node shutdown failed: " + node.getUuid() + "/" + node.getIp4() + ": " + e);
if (validationResult.isInvalid()) {
throw invalidEx(validationResult);
}
});

if (validationResult.isInvalid()) throw invalidEx(validationResult);
// delete nodes in network
nodes.forEach(node -> nodeDAO.delete(node.getUuid()));

network.setState(BubbleNetworkState.stopped);
networkDAO.update(network);
log.info("stopNetwork: stopped " + network.getNetworkDomain());
network.setState(BubbleNetworkState.stopped);
networkDAO.update(network);
stopped = true;

// delete nodes in network
nodes.forEach(node -> nodeDAO.delete(node.getUuid()));
} catch (RuntimeException e) {
log.error("stopNetwork: error stopping: "+e);
if (network != null) network.setState(BubbleNetworkState.error_stopping);
networkDAO.update(network);
throw e;

log.info("stopNetwork: stopped " + network.getNetworkDomain());
return true;
} finally {
if (lock != null) unlockNetwork(networkUuid, lock);
}
return stopped;
}

protected CloudService findServiceOrDelegate(String cloudUuid) {


+ 23
- 8
bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties View File

@@ -166,12 +166,6 @@ label_field_networks_name=Name
label_field_networks_locale=Locale
label_field_networks_timezone=Time Zone
label_field_networks_object_state=State
label_field_networks_action_view=View
label_field_networks_action_stop=Stop
label_field_networks_action_delete=Delete
table_row_networks_action_view=View
table_row_networks_action_stop=Stop
table_row_networks_action_delete=Delete
button_label_new_network=Create Bubble
message_empty_networks=Create your first Bubble!

@@ -206,6 +200,24 @@ button_label_customize=Customize
button_label_use_default=Use Default
button_label_create_new_network=Create New Bubble

# Network Page - Restore Keys
link_network_action_request_keys=Request Bubble Restore Key
message_network_action_keys_requested=Bubble Restore Key requested
message_network_action_retrieve_keys=Download Bubble Restore Key
message_network_keys=Bubble Restore Key
message_network_keys_description=Save this to a secure location. Keep it separate from the password you used to encrypt it.
field_network_key_download_code=Download Code
field_network_key_download_password=Encrypt with password
button_label_retrieve_keys=Download
err.retrieveNetworkKeys.notFound=Download Code Not Found

# Network Page - Danger Zone
title_network_danger_zone=Danger Zone
link_network_action_stop=Stop
link_network_action_stop_description=Stop this Bubble. If you have downloaded your restore key, you can later restore it.
link_network_action_delete=Delete
link_network_action_delete_description=Delete this Bubble and all backups. You will not be able to restore this Bubble. This action cannot be undone.

# Bubble Plans
plan_name_bubble=Bubble Standard
plan_description_bubble=Try this one first. Most users probably don't need anything more.
@@ -263,7 +275,7 @@ meter_tick_apt_install_done=Installing system packages
meter_tick_pip_install=Installing python packages
meter_tick_pyyaml_pycparser=Continuing to install python packages
meter_tick_playbook_start=Running configuration playbook
meter_tick_role_common=Installing common Bubble functionality
meter_tick_role_common=Installing core system packages
meter_tick_role_common_packages=Installing Bubble packages
meter_tick_role_firewall=Setting up firewall
meter_tick_role_bubble=Installing Bubble API
@@ -275,7 +287,7 @@ meter_tick_role_nginx=Setting up web server
meter_tick_role_nginx_certbot=Installing SSL certificates
meter_tick_role_mitmproxy=Setting up MITM server
meter_tick_role_bubble_finalizer=Finalizing Bubble installation
meter_tick_role_bubble_finalizer_touch=Turning on "first-time" setting to allow user to unlock Bubble
meter_tick_role_bubble_finalizer_touch=Turning on "first-time" setting to allow you to unlock your Bubble
meter_tick_role_bubble_finalizer_start=Starting Bubble API services
meter_tick_install_complete=Bubble installation completed

@@ -302,6 +314,7 @@ msg_network_state_starting=starting
msg_network_state_restoring=restoring
msg_network_state_running=running
msg_network_state_stopping=stopping
msg_network_state_error_stopping=error stopping
msg_network_state_stopped=stopped

# Node states
@@ -398,6 +411,7 @@ 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
@@ -408,6 +422,7 @@ 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
err.networkKeys.noVerifiedContacts=Cannot download network keys, no account contact information has yet been verified
err.networkKeys.invalid=Bubble Restore Key was not valid
err.networkName.required=Network name is required
err.network.cannotStartInCurrentState=Cannot proceed: network cannot be started in its current state
err.network.required=Network is required


+ 5
- 3
bubble-server/src/test/resources/models/include/get_network_keys.json View File

@@ -5,7 +5,8 @@
"params": {
"network": "_required",
"rootEmail": "root@example.com",
"networkKeysVar": "networkKeys"
"networkKeysVar": "networkKeys",
"networkKeysPassword": "Passw0rd!"
}
},

@@ -59,12 +60,13 @@
{
"comment": "use confirmation token in email to retrieve network keys",
"request": {
"uri": "me/networks/<<network>>/actions/keys/{{emailInbox.[0].ctx.message.requestId}}"
"uri": "me/networks/<<network>>/actions/keys/{{emailInbox.[0].ctx.message.requestId}}",
"entity": { "name": "password", "value": "<<networkKeysPassword>>" }
},
"response": {
"store": "<<networkKeysVar>>",
"check": [
{"condition": "json.getKeys().length >= 1"}
{"condition": "json.getData().length >= 1"}
]
}
}

+ 6
- 2
bubble-server/src/test/resources/models/tests/live/backup_and_restore.json View File

@@ -147,7 +147,8 @@
"params": {
"network": "{{bubbleNetwork.network}}",
"rootEmail": "bubble-user@example.com",
"networkKeysVar": "networkKeys"
"networkKeysVar": "networkKeys",
"networkKeysPassword": "Passw0rd!!"
}
},

@@ -195,7 +196,10 @@
"comment": "restore node using restoreKey",
"request": {
"uri": "auth/restore/{{restoreNN.restoreKey}}",
"entityJson": "{{json networkKeys}}",
"entity": {
"data": "{{networkKeys.data}}",
"password": "Passw0rd!!"
},
"method": "put"
},
"after": "sleep 240s" // give the restore some time to stop the server, restore and restart


+ 42
- 8
bubble-server/src/test/resources/models/tests/network/network_keys.json View File

@@ -69,15 +69,44 @@
}
},

{
"comment": "use confirmation token in email to retrieve network keys, fails because password is not supplied",
"request": {
"uri": "me/networks/{{serverConfig.thisNetwork.uuid}}/actions/keys/{{emailInbox.[0].ctx.message.requestId}}",
"method": "post"
},
"response": {
"status": 422,
"check": [
{"condition": "json.has('err.password.required')"}
]
}
},

{
"comment": "use confirmation token in email to retrieve network keys, fails because password is too simple",
"request": {
"uri": "me/networks/{{serverConfig.thisNetwork.uuid}}/actions/keys/{{emailInbox.[0].ctx.message.requestId}}",
"entity": { "name": "password", "value": "password" }
},
"response": {
"status": 422,
"check": [
{"condition": "json.has('err.password.invalid')"}
]
}
},

{
"comment": "use confirmation token in email to retrieve network keys",
"request": {
"uri": "me/networks/{{serverConfig.thisNetwork.uuid}}/actions/keys/{{emailInbox.[0].ctx.message.requestId}}"
"uri": "me/networks/{{serverConfig.thisNetwork.uuid}}/actions/keys/{{emailInbox.[0].ctx.message.requestId}}",
"entity": { "name": "password", "value": "Passw0rd!" }
},
"response": {
"store": "networkKeys",
"check": [
{"condition": "json.getKeys().length >= 1"}
{"condition": "json.getData().length >= 1"}
]
}
},
@@ -85,10 +114,12 @@
{
"comment": "try to retrieve keys again, fails because it's a one-time operation",
"request": {
"uri": "me/networks/{{serverConfig.thisNetwork.uuid}}/actions/keys/{{emailInbox.[0].ctx.message.requestId}}"
"uri": "me/networks/{{serverConfig.thisNetwork.uuid}}/actions/keys/{{emailInbox.[0].ctx.message.requestId}}",
"entity": { "name": "password", "value": "Passw0rd!" }
},
"response": {
"status": 404
"status": 422,
"check": [ {"condition": "json.has('err.retrieveNetworkKeys.notFound')"} ]
}
},

@@ -210,12 +241,13 @@
{
"comment": "use confirmation token in email to retrieve network keys",
"request": {
"uri": "me/networks/{{serverConfig.thisNetwork.uuid}}/actions/keys/{{emailInbox.[0].ctx.message.requestId}}"
"uri": "me/networks/{{serverConfig.thisNetwork.uuid}}/actions/keys/{{emailInbox.[0].ctx.message.requestId}}",
"entity": { "name": "password", "value": "Passw0rd!" }
},
"response": {
"store": "networkKeys",
"check": [
{"condition": "json.getKeys().length >= 1"}
{"condition": "json.getData().length >= 1"}
]
}
},
@@ -223,10 +255,12 @@
{
"comment": "try to retrieve keys again, fails because it's a one-time operation",
"request": {
"uri": "me/networks/{{serverConfig.thisNetwork.uuid}}/actions/keys/{{smsInbox.[0].ctx.message.requestId}}"
"uri": "me/networks/{{serverConfig.thisNetwork.uuid}}/actions/keys/{{smsInbox.[0].ctx.message.requestId}}",
"entity": { "name": "password", "value": "Passw0rd!" }
},
"response": {
"status": 404
"status": 422,
"check": [ {"condition": "json.has('err.retrieveNetworkKeys.notFound')"} ]
}
}
]

+ 1
- 1
bubble-web

@@ -1 +1 @@
Subproject commit e8c5f6c80992169184d4a17c1c65bfc5424a0686
Subproject commit 54759b23204ed1019c71e1f5414cb92f184e4aaa

+ 1
- 1
utils/cobbzilla-utils

@@ -1 +1 @@
Subproject commit 0316b9e68650414d2fd051d704aa411aca1a0568
Subproject commit 39ab28e818c43b75eca844ef41fb466eb5e5a114

Loading…
Cancel
Save