From f9ff2565a23138a5a387ace1e5122f42e50ada85 Mon Sep 17 00:00:00 2001 From: Jonathan Cobb Date: Sat, 11 Jan 2020 02:12:15 -0500 Subject: [PATCH] encrypt network restore keys, add ui support for stop/delete/restore bubble --- .../java/bubble/dao/bill/AccountPlanDAO.java | 1 + .../bubble/dao/cloud/CloudServiceDAO.java | 2 +- .../bubble/model/account/AccountContact.java | 9 ++- .../bubble/model/account/AccountPolicy.java | 6 ++ .../model/cloud/BubbleNetworkState.java | 2 +- .../java/bubble/model/cloud/BubbleNode.java | 1 + .../java/bubble/model/cloud/NetworkKeys.java | 24 +++++- .../resources/account/AuthResource.java | 13 +++- .../cloud/NetworkActionsResource.java | 17 +++- .../service/cloud/StandardNetworkService.java | 78 ++++++++++++------- .../post_auth/ResourceMessages.properties | 31 ++++++-- .../models/include/get_network_keys.json | 8 +- .../models/tests/live/backup_and_restore.json | 8 +- .../models/tests/network/network_keys.json | 50 ++++++++++-- bubble-web | 2 +- utils/cobbzilla-utils | 2 +- 16 files changed, 191 insertions(+), 63 deletions(-) diff --git a/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java b/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java index 87311c64..e4dafeb3 100644 --- a/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java +++ b/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java @@ -130,6 +130,7 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO { throw invalidEx("err.accountPlan.stopNetworkBeforeDeleting"); } update(accountPlan.setDeleted(now()).setEnabled(false)); + networkDAO.delete(accountPlan.getNetwork()); if (configuration.paymentsEnabled()) { refundService.processRefunds(); } diff --git a/bubble-server/src/main/java/bubble/dao/cloud/CloudServiceDAO.java b/bubble-server/src/main/java/bubble/dao/cloud/CloudServiceDAO.java index 3d16eaf8..43141a01 100644 --- a/bubble-server/src/main/java/bubble/dao/cloud/CloudServiceDAO.java +++ b/bubble-server/src/main/java/bubble/dao/cloud/CloudServiceDAO.java @@ -41,7 +41,7 @@ public class CloudServiceDAO extends AccountOwnedTemplateDAO { } @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); } diff --git a/bubble-server/src/main/java/bubble/model/account/AccountContact.java b/bubble-server/src/main/java/bubble/model/account/AccountContact.java index c8595b74..e6fb0291 100644 --- a/bubble-server/src/main/java/bubble/model/account/AccountContact.java +++ b/bubble-server/src/main/java/bubble/model/account/AccountContact.java @@ -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 mask(Collection contacts) { + return empty(contacts) ? contacts : contacts.stream().map(c -> c.mask()).collect(Collectors.toList()); } public ValidationResult validate(ValidationResult errors) { diff --git a/bubble-server/src/main/java/bubble/model/account/AccountPolicy.java b/bubble-server/src/main/java/bubble/model/account/AccountPolicy.java index f14f31fe..7c290c2a 100644 --- a/bubble-server/src/main/java/bubble/model/account/AccountPolicy.java +++ b/bubble-server/src/main/java/bubble/model/account/AccountPolicy.java @@ -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 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); } diff --git a/bubble-server/src/main/java/bubble/model/cloud/BubbleNetworkState.java b/bubble-server/src/main/java/bubble/model/cloud/BubbleNetworkState.java index e7fc3fbe..0a35e18a 100644 --- a/bubble-server/src/main/java/bubble/model/cloud/BubbleNetworkState.java +++ b/bubble-server/src/main/java/bubble/model/cloud/BubbleNetworkState.java @@ -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); } diff --git a/bubble-server/src/main/java/bubble/model/cloud/BubbleNode.java b/bubble-server/src/main/java/bubble/model/cloud/BubbleNode.java index cffc40cf..766f0044 100644 --- a/bubble-server/src/main/java/bubble/model/cloud/BubbleNode.java +++ b/bubble-server/src/main/java/bubble/model/cloud/BubbleNode.java @@ -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) diff --git a/bubble-server/src/main/java/bubble/model/cloud/NetworkKeys.java b/bubble-server/src/main/java/bubble/model/cloud/NetworkKeys.java index 3dc436a5..f4f26f55 100644 --- a/bubble-server/src/main/java/bubble/model/cloud/NetworkKeys.java +++ b/bubble-server/src/main/java/bubble/model/cloud/NetworkKeys.java @@ -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; + } } diff --git a/bubble-server/src/main/java/bubble/resources/account/AuthResource.java b/bubble-server/src/main/java/bubble/resources/account/AuthResource.java index 258b70f3..ad8248ec 100644 --- a/bubble-server/src/main/java/bubble/resources/account/AuthResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/AuthResource.java @@ -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())); diff --git a/bubble-server/src/main/java/bubble/resources/cloud/NetworkActionsResource.java b/bubble-server/src/main/java/bubble/resources/cloud/NetworkActionsResource.java index 6bfd6251..4da19a04 100644 --- a/bubble-server/src/main/java/bubble/resources/cloud/NetworkActionsResource.java +++ b/bubble-server/src/main/java/bubble/resources/cloud/NetworkActionsResource.java @@ -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) diff --git a/bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java b/bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java index 6022dc1b..2910fd92 100644 --- a/bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java +++ b/bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java @@ -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 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 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) { diff --git a/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties b/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties index b78d390d..f896f243 100644 --- a/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties +++ b/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties @@ -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 diff --git a/bubble-server/src/test/resources/models/include/get_network_keys.json b/bubble-server/src/test/resources/models/include/get_network_keys.json index 903c13cf..02ab23c8 100644 --- a/bubble-server/src/test/resources/models/include/get_network_keys.json +++ b/bubble-server/src/test/resources/models/include/get_network_keys.json @@ -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/<>/actions/keys/{{emailInbox.[0].ctx.message.requestId}}" + "uri": "me/networks/<>/actions/keys/{{emailInbox.[0].ctx.message.requestId}}", + "entity": { "name": "password", "value": "<>" } }, "response": { "store": "<>", "check": [ - {"condition": "json.getKeys().length >= 1"} + {"condition": "json.getData().length >= 1"} ] } } diff --git a/bubble-server/src/test/resources/models/tests/live/backup_and_restore.json b/bubble-server/src/test/resources/models/tests/live/backup_and_restore.json index 94819645..90506a01 100644 --- a/bubble-server/src/test/resources/models/tests/live/backup_and_restore.json +++ b/bubble-server/src/test/resources/models/tests/live/backup_and_restore.json @@ -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 diff --git a/bubble-server/src/test/resources/models/tests/network/network_keys.json b/bubble-server/src/test/resources/models/tests/network/network_keys.json index f062767c..9f4e57e7 100644 --- a/bubble-server/src/test/resources/models/tests/network/network_keys.json +++ b/bubble-server/src/test/resources/models/tests/network/network_keys.json @@ -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')"} ] } } ] \ No newline at end of file diff --git a/bubble-web b/bubble-web index e8c5f6c8..54759b23 160000 --- a/bubble-web +++ b/bubble-web @@ -1 +1 @@ -Subproject commit e8c5f6c80992169184d4a17c1c65bfc5424a0686 +Subproject commit 54759b23204ed1019c71e1f5414cb92f184e4aaa diff --git a/utils/cobbzilla-utils b/utils/cobbzilla-utils index 0316b9e6..39ab28e8 160000 --- a/utils/cobbzilla-utils +++ b/utils/cobbzilla-utils @@ -1 +1 @@ -Subproject commit 0316b9e68650414d2fd051d704aa411aca1a0568 +Subproject commit 39ab28e818c43b75eca844ef41fb466eb5e5a114