From 7cba082bc0fda37018c67d319e6e5d55f4fcc1c9 Mon Sep 17 00:00:00 2001 From: Jonathan Cobb Date: Sun, 12 Jan 2020 02:03:55 -0500 Subject: [PATCH] use authenticator. refactor tests to use example domain and mock dns --- .../src/main/java/bubble/ApiConstants.java | 13 +- .../cloud/auth/AuthenticationDriver.java | 2 - .../bubble/cloud/dns/mock/MockDnsDriver.java | 37 ++++ .../cloud/storage/StorageServiceDriver.java | 2 +- .../java/bubble/main/BubbleScriptMain.java | 5 +- .../bubble/model/account/AccountContact.java | 23 +-- .../bubble/model/account/AccountPolicy.java | 52 +++++- .../model/account/AuthenticatorRequest.java | 5 + .../model/account/message/AccountMessage.java | 2 +- .../model/account/message/ActionTarget.java | 2 +- .../main/java/bubble/model/app/BubbleApp.java | 1 + .../java/bubble/model/app/RuleDriver.java | 1 + .../java/bubble/model/cloud/AnsibleRole.java | 1 + .../java/bubble/model/cloud/BubbleDomain.java | 3 +- .../bubble/model/cloud/BubbleFootprint.java | 1 + .../bubble/notify/NewNodeNotification.java | 12 ++ .../bubble/resources/SessionsResource.java | 35 ---- .../account/AccountOwnedResource.java | 1 + .../resources/account/AccountsResource.java | 30 ++- .../resources/account/AuthResource.java | 56 +++--- .../bubble/resources/account/MeResource.java | 11 ++ .../resources/bill/AccountPlansResource.java | 16 ++ .../cloud/NetworkActionsResource.java | 15 +- .../bubble/server/BubbleConfiguration.java | 17 +- .../bubble/service/AuthenticatorService.java | 79 ++++++++ .../StandardAccountMessageService.java | 4 - .../bubble/service/backup/BackupService.java | 3 +- .../service/boot/ActivationService.java | 23 ++- .../post_auth/ResourceMessages.properties | 9 +- .../pre_auth/ResourceMessages.properties | 6 +- .../test/ActivatedBubbleModelTestBase.java | 47 +++-- .../src/test/java/bubble/test/AuthTest.java | 5 +- .../src/test/java/bubble/test/BackupTest.java | 11 ++ .../bubble/test/BubbleApiRunnerListener.java | 13 +- .../java/bubble/test/BubbleCoreSuite.java | 4 +- .../java/bubble/test/LiveNetworkTest.java | 15 ++ .../java/bubble/test/NetworkKeysTest.java | 9 + .../test/java/bubble/test/NetworkTest.java | 7 - .../java/bubble/test/NetworkTestBase.java | 4 +- .../test/java/bubble/test/PaymentTest.java | 2 +- .../models/include/add_authenticator.json | 57 ++++++ .../resources/models/manifest-payment.json | 7 - .../test/resources/models/manifest-test.json | 11 ++ .../resources/models/system/bubbleDomain.json | 47 +---- .../models/system/cloudService_test.json | 8 + .../models/tests/auth/multifactor_auth.json | 46 +---- .../models/tests/auth/network_auth.json | 171 ++++++++++++++++++ utils/cobbzilla-utils | 2 +- utils/cobbzilla-wizard | 2 +- 49 files changed, 679 insertions(+), 256 deletions(-) create mode 100644 bubble-server/src/main/java/bubble/cloud/dns/mock/MockDnsDriver.java delete mode 100644 bubble-server/src/main/java/bubble/resources/SessionsResource.java create mode 100644 bubble-server/src/main/java/bubble/service/AuthenticatorService.java create mode 100644 bubble-server/src/test/java/bubble/test/BackupTest.java create mode 100644 bubble-server/src/test/java/bubble/test/LiveNetworkTest.java create mode 100644 bubble-server/src/test/java/bubble/test/NetworkKeysTest.java create mode 100644 bubble-server/src/test/resources/models/include/add_authenticator.json delete mode 100644 bubble-server/src/test/resources/models/manifest-payment.json create mode 100644 bubble-server/src/test/resources/models/manifest-test.json create mode 100644 bubble-server/src/test/resources/models/tests/auth/network_auth.json diff --git a/bubble-server/src/main/java/bubble/ApiConstants.java b/bubble-server/src/main/java/bubble/ApiConstants.java index e2186ac3..12cba439 100644 --- a/bubble-server/src/main/java/bubble/ApiConstants.java +++ b/bubble-server/src/main/java/bubble/ApiConstants.java @@ -15,6 +15,7 @@ import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; import java.util.function.Supplier; @@ -37,19 +38,24 @@ public class ApiConstants { public static final String DEFAULT_LOCALE = "en_US"; - @Getter(lazy=true) private static final String bubbleDefaultDomain = initDefaultDomain(); + private static final AtomicReference bubbleDefaultDomain = new AtomicReference<>(); private static String initDefaultDomain() { final File f = new File(HOME_DIR, ".BUBBLE_DEFAULT_DOMAIN"); final String domain = FileUtil.toStringOrDie(f); return domain != null ? domain.trim() : die("initDefaultDomain: "+abs(f)+" not found"); } + public static String getBubbleDefaultDomain () { + synchronized (bubbleDefaultDomain) { + if (bubbleDefaultDomain.get() == null) bubbleDefaultDomain.set(initDefaultDomain()); + } + return bubbleDefaultDomain.get(); + } + public static final BubbleNode NULL_NODE = new BubbleNode() { @Override public String getUuid() { return "NULL_UUID"; } }; - public static final String ENV_BUBBLE_JAR = "BUBBLE_JAR"; - public static final GoogleAuthenticator G_AUTH = new GoogleAuthenticator(); public static final Predicate ALWAYS_TRUE = m -> true; @@ -87,7 +93,6 @@ public class ApiConstants { public static final String EP_REQUEST = "/request"; public static final String EP_DOWNLOAD = "/download"; - public static final String SESSIONS_ENDPOINT = "/sessions"; public static final String MESSAGES_ENDPOINT = "/messages"; public static final String TIMEZONES_ENDPOINT = "/timezones"; diff --git a/bubble-server/src/main/java/bubble/cloud/auth/AuthenticationDriver.java b/bubble-server/src/main/java/bubble/cloud/auth/AuthenticationDriver.java index 80e54552..d854d920 100644 --- a/bubble-server/src/main/java/bubble/cloud/auth/AuthenticationDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/auth/AuthenticationDriver.java @@ -65,7 +65,6 @@ public interface AuthenticationDriver extends CloudServiceDriver { static BubbleNode getNode(AccountMessage message, BubbleConfiguration configuration) { switch (message.getTarget()) { case account: case network: return configuration.getThisNode(); - case node: return configuration.getBean(BubbleNodeDAO.class).findByAccountAndId(message.getAccount(), message.getName()); default: return null; } } @@ -79,7 +78,6 @@ public interface AuthenticationDriver extends CloudServiceDriver { switch (message.getTarget()) { case account: return configuration.getThisNetwork(); case network: return networkDAO.findByAccountAndId(message.getAccount(), message.getName()); - case node: return networkDAO.findByAccountAndId(message.getAccount(), node.getNetwork()); default: return null; } } diff --git a/bubble-server/src/main/java/bubble/cloud/dns/mock/MockDnsDriver.java b/bubble-server/src/main/java/bubble/cloud/dns/mock/MockDnsDriver.java new file mode 100644 index 00000000..10e909db --- /dev/null +++ b/bubble-server/src/main/java/bubble/cloud/dns/mock/MockDnsDriver.java @@ -0,0 +1,37 @@ +package bubble.cloud.dns.mock; + +import bubble.cloud.config.CloudApiConfig; +import bubble.cloud.dns.DnsDriverBase; +import bubble.model.cloud.BubbleDomain; +import org.cobbzilla.util.dns.DnsRecord; +import org.cobbzilla.util.dns.DnsRecordMatch; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class MockDnsDriver extends DnsDriverBase { + + private Map records = new ConcurrentHashMap<>(); + + private String recordKey(DnsRecord record) { return record.getType()+":"+record.getFqdn(); } + + @Override public Collection create(BubbleDomain domain) { return Collections.emptyList(); } + + @Override public DnsRecord update(DnsRecord record) { + records.put(recordKey(record), record); + return record; + } + + @Override public DnsRecord remove(DnsRecord record) { + records.remove(recordKey(record)); + return record; + } + + @Override public Collection list(DnsRecordMatch matcher) { + return records.values().stream().filter(matcher::matches).collect(Collectors.toList()); + } + +} diff --git a/bubble-server/src/main/java/bubble/cloud/storage/StorageServiceDriver.java b/bubble-server/src/main/java/bubble/cloud/storage/StorageServiceDriver.java index a2e7cdae..ac95b3fb 100644 --- a/bubble-server/src/main/java/bubble/cloud/storage/StorageServiceDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/storage/StorageServiceDriver.java @@ -7,7 +7,7 @@ import bubble.model.cloud.StorageMetadata; import bubble.notify.storage.StorageListing; import lombok.Cleanup; import org.apache.commons.io.IOUtils; -import org.cobbzilla.util.daemon.ExceptionHandler; +import org.cobbzilla.util.error.ExceptionHandler; import org.cobbzilla.util.string.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/bubble-server/src/main/java/bubble/main/BubbleScriptMain.java b/bubble-server/src/main/java/bubble/main/BubbleScriptMain.java index 35a418d9..a4a7efe5 100644 --- a/bubble-server/src/main/java/bubble/main/BubbleScriptMain.java +++ b/bubble-server/src/main/java/bubble/main/BubbleScriptMain.java @@ -1,10 +1,11 @@ package bubble.main; -import bubble.ApiConstants; import bubble.server.BubbleConfiguration; import java.util.Map; +import static bubble.ApiConstants.getBubbleDefaultDomain; + public class BubbleScriptMain extends BubbleScriptMainBase { public static final BubbleConfiguration DEFAULT_BUBBLE_CONFIG = new BubbleConfiguration(); @@ -12,7 +13,7 @@ public class BubbleScriptMain extends BubbleScriptMainBase public static void main (String[] args) { main(BubbleScriptMain.class, args); } @Override protected void setScriptContextVars(Map ctx) { - ctx.put("defaultDomain", ApiConstants.getBubbleDefaultDomain()); + ctx.put("defaultDomain", getBubbleDefaultDomain()); ctx.put("serverConfig", DEFAULT_BUBBLE_CONFIG); super.setScriptContextVars(ctx); } 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 d86914d3..adc3acfc 100644 --- a/bubble-server/src/main/java/bubble/model/account/AccountContact.java +++ b/bubble-server/src/main/java/bubble/model/account/AccountContact.java @@ -19,10 +19,8 @@ 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; @@ -117,12 +115,7 @@ public class AccountContact implements Serializable { if (c.hasNick() && c.getNick().length() > MAX_NICK_LENGTH) { throw invalidEx("err.nick.tooLong"); } - if (c.isAuthenticator()) { - // can only change nick on authenticator - if (c.hasNick()) existing.setNick(c.getNick()); - } else { - copy(existing, c); - } + existing.update(c); } else { // creating a new contact -- cannot set authFactor for contacts requiring verification @@ -267,8 +260,8 @@ public class AccountContact implements Serializable { case start: case stop: case delete: switch (target) { - case account: return bool(requiredForAccountOperations); - case node: case network: return bool(requiredForNetworkOperations); + case account: return bool(requiredForAccountOperations); + case network: return bool(requiredForNetworkOperations); default: log.warn("isAllowed(start/stop/delete): unknown target: "+target+", returning false"); return false; @@ -284,12 +277,12 @@ public class AccountContact implements Serializable { } } - public AccountContact mask() { - return new AccountContact(this).setInfo(getType().mask(getInfo())); - } + public AccountContact mask() { 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 static AccountContact[] mask(List contacts) { + return contacts.stream() + .map(AccountContact::mask) + .toArray(AccountContact[]::new); } 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 634a5054..695fc906 100644 --- a/bubble-server/src/main/java/bubble/model/account/AccountPolicy.java +++ b/bubble-server/src/main/java/bubble/model/account/AccountPolicy.java @@ -23,10 +23,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static bubble.model.account.AccountContact.contactMatch; +import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.MINUTES; import static org.cobbzilla.util.daemon.ZillaRuntime.die; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; @@ -38,10 +38,13 @@ import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.*; @NoArgsConstructor @Accessors(chain=true) public class AccountPolicy extends IdentifiableBase implements HasAccount { - public static final Long MAX_ACCOUNT_OPERATION_TIMEOUT = TimeUnit.DAYS.toMillis(3); - public static final Long MAX_NODE_OPERATION_TIMEOUT = TimeUnit.DAYS.toMillis(3); + public static final Long MAX_ACCOUNT_OPERATION_TIMEOUT = DAYS.toMillis(3); + public static final Long MAX_NODE_OPERATION_TIMEOUT = DAYS.toMillis(3); + public static final Long MAX_AUTHENTICATOR_TIMEOUT = DAYS.toMillis(1); + public static final Long MIN_ACCOUNT_OPERATION_TIMEOUT = MINUTES.toMillis(1); - public static final Long MIN_NODE_OPERATION_TIMEOUT = MINUTES.toMillis(1); + public static final Long MIN_NODE_OPERATION_TIMEOUT = MINUTES.toMillis(1); + public static final Long MIN_AUTHENTICATOR_TIMEOUT = MINUTES.toMillis(1); public static final String[] UPDATE_FIELDS = {"deletionPolicy", "nodeOperationTimeout", "accountOperationTimeout"}; @@ -64,14 +67,18 @@ public class AccountPolicy extends IdentifiableBase implements HasAccount { @Type(type=ENCRYPTED_LONG) @Column(columnDefinition="varchar("+ENC_LONG+") NOT NULL") @Getter @Setter private Long accountOperationTimeout = MINUTES.toMillis(10); - @ECSearchable @ECField(index=40) + @ECSearchable(type=EntityFieldType.time_duration) @ECField(index=40) + @Type(type=ENCRYPTED_LONG) @Column(columnDefinition="varchar("+ENC_LONG+") NOT NULL") + @Getter @Setter private Long authenticatorTimeout = MAX_AUTHENTICATOR_TIMEOUT; + + @ECSearchable @ECField(index=50) @Enumerated(EnumType.STRING) @Column(length=40, nullable=false) @Getter @Setter private AccountDeletionPolicy deletionPolicy = AccountDeletionPolicy.block_delete; @JsonIgnore @Transient public boolean isFullDelete () { return deletionPolicy == AccountDeletionPolicy.full_delete; } @JsonIgnore @Transient public boolean isBlockDelete () { return deletionPolicy == AccountDeletionPolicy.block_delete; } - @ECSearchable(filter=true) @ECField(index=50) + @ECSearchable(filter=true) @ECField(index=60) @Size(max=100000, message="err.accountContactsJson.length") @Type(type=ENCRYPTED_STRING) @Column(columnDefinition="varchar("+(100000+ENC_PAD)+")") @JsonIgnore @Getter @Setter private String accountContactsJson; @@ -113,7 +120,7 @@ public class AccountPolicy extends IdentifiableBase implements HasAccount { .filter(c -> c.requiredForAccountOperations() || c.requiredAuthFactor()) .collect(Collectors.toList()); } - case network: case node: + case network: return Arrays.stream(getAccountContacts()) .filter(c -> c.requiredForNetworkOperations() || c.requiredAuthFactor()) .collect(Collectors.toList()); @@ -145,6 +152,22 @@ public class AccountPolicy extends IdentifiableBase implements HasAccount { return contact == null ? null : contact.getAuthFactor(); } + @Transient @JsonIgnore public List getAccountAuthFactors() { + if (!hasAccountContacts()) return Collections.emptyList(); + return Arrays.stream(getAccountContacts()) + .filter(AccountContact::authFactor) + .filter(AccountContact::requiredForAccountOperations) + .collect(Collectors.toList()); + } + + @Transient @JsonIgnore public List getNetworkAuthFactors() { + if (!hasAccountContacts()) return Collections.emptyList(); + return Arrays.stream(getAccountContacts()) + .filter(AccountContact::authFactor) + .filter(AccountContact::requiredForAccountOperations) + .collect(Collectors.toList()); + } + public String contactsString () { final StringBuilder b = new StringBuilder(); if (hasAccountContacts()) { @@ -195,13 +218,17 @@ public class AccountPolicy extends IdentifiableBase implements HasAccount { if (!hasAccountContacts()) return null; return Arrays.stream(getAccountContacts()).filter(AccountContact::isAuthenticator).findFirst().orElse(null); } + public boolean hasVerifiedAuthenticator () { + final AccountContact authenticator = getAuthenticator(); + return authenticator != null && authenticator.verified(); + } public Long getTimeout(AccountMessage message) { return getTimeout(message.getTarget()); } public Long getTimeout(ActionTarget target) { switch (target) { - case account: return accountOperationTimeout; - case node: case network: return nodeOperationTimeout; + case account: return accountOperationTimeout; + case network: return nodeOperationTimeout; default: return die("getTimeout: invalid target: "+ target); } } @@ -240,6 +267,13 @@ public class AccountPolicy extends IdentifiableBase implements HasAccount { } else if (nodeOperationTimeout < MIN_NODE_OPERATION_TIMEOUT) { result.addViolation("err.nodeOperationTimeout.tooShort"); } + if (authenticatorTimeout == null) { + result.addViolation("err.authenticatorTimeout.required"); + } else if (authenticatorTimeout > MAX_AUTHENTICATOR_TIMEOUT) { + result.addViolation("err.authenticatorTimeout.tooLong"); + } else if (authenticatorTimeout < MIN_AUTHENTICATOR_TIMEOUT) { + result.addViolation("err.authenticatorTimeout.tooShort"); + } return result; } diff --git a/bubble-server/src/main/java/bubble/model/account/AuthenticatorRequest.java b/bubble-server/src/main/java/bubble/model/account/AuthenticatorRequest.java index 95962068..8ae7897d 100644 --- a/bubble-server/src/main/java/bubble/model/account/AuthenticatorRequest.java +++ b/bubble-server/src/main/java/bubble/model/account/AuthenticatorRequest.java @@ -19,4 +19,9 @@ public class AuthenticatorRequest { @Getter @Setter private Boolean verify; public boolean verify() { return bool(verify); } + @Getter @Setter private Boolean authenticate; + public boolean authenticate() { return bool(authenticate); } + + public boolean startSession() { return !verify() && !authenticate(); } + } diff --git a/bubble-server/src/main/java/bubble/model/account/message/AccountMessage.java b/bubble-server/src/main/java/bubble/model/account/message/AccountMessage.java index 9156b33f..42c3ff74 100644 --- a/bubble-server/src/main/java/bubble/model/account/message/AccountMessage.java +++ b/bubble-server/src/main/java/bubble/model/account/message/AccountMessage.java @@ -83,7 +83,7 @@ public class AccountMessage extends IdentifiableBase implements HasAccount { if (getMessageType() != AccountMessageType.request) return -1; switch (getTarget()) { case account: return policy.getAccountOperationTimeout()/1000; - case node: case network: return policy.getNodeOperationTimeout()/1000; + case network: return policy.getNodeOperationTimeout()/1000; default: log.warn("tokenTimeout: invalid target: "+getTarget()); return -1; diff --git a/bubble-server/src/main/java/bubble/model/account/message/ActionTarget.java b/bubble-server/src/main/java/bubble/model/account/message/ActionTarget.java index 10647f97..7726694f 100644 --- a/bubble-server/src/main/java/bubble/model/account/message/ActionTarget.java +++ b/bubble-server/src/main/java/bubble/model/account/message/ActionTarget.java @@ -6,7 +6,7 @@ import static bubble.ApiConstants.enumFromString; public enum ActionTarget { - node, network, account; + network, account; @JsonCreator public static ActionTarget fromString (String v) { return enumFromString(ActionTarget.class, v); } diff --git a/bubble-server/src/main/java/bubble/model/app/BubbleApp.java b/bubble-server/src/main/java/bubble/model/app/BubbleApp.java index 5ed6ca53..faa8d38a 100644 --- a/bubble-server/src/main/java/bubble/model/app/BubbleApp.java +++ b/bubble-server/src/main/java/bubble/model/app/BubbleApp.java @@ -18,6 +18,7 @@ import javax.validation.constraints.Size; import static bubble.ApiConstants.APPS_ENDPOINT; import static bubble.ApiConstants.EP_APPS; +import static org.cobbzilla.util.daemon.ZillaRuntime.bool; import static org.cobbzilla.util.reflect.ReflectionUtil.copy; import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENCRYPTED_STRING; import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENC_PAD; diff --git a/bubble-server/src/main/java/bubble/model/app/RuleDriver.java b/bubble-server/src/main/java/bubble/model/app/RuleDriver.java index 2290c794..59169404 100644 --- a/bubble-server/src/main/java/bubble/model/app/RuleDriver.java +++ b/bubble-server/src/main/java/bubble/model/app/RuleDriver.java @@ -26,6 +26,7 @@ import java.util.Locale; import static bubble.ApiConstants.DRIVERS_ENDPOINT; import static bubble.ApiConstants.EP_DRIVERS; import static bubble.server.BubbleConfiguration.getDEFAULT_LOCALE; +import static org.cobbzilla.util.daemon.ZillaRuntime.bool; import static org.cobbzilla.util.io.StreamUtil.stream2string; import static org.cobbzilla.util.json.JsonUtil.json; import static org.cobbzilla.util.reflect.ReflectionUtil.copy; diff --git a/bubble-server/src/main/java/bubble/model/cloud/AnsibleRole.java b/bubble-server/src/main/java/bubble/model/cloud/AnsibleRole.java index eed1d08b..a9d0ca5d 100644 --- a/bubble-server/src/main/java/bubble/model/cloud/AnsibleRole.java +++ b/bubble-server/src/main/java/bubble/model/cloud/AnsibleRole.java @@ -21,6 +21,7 @@ import java.util.Arrays; import static bubble.ApiConstants.EP_ROLES; import static bubble.cloud.storage.StorageServiceDriver.STORAGE_PREFIX; +import static org.cobbzilla.util.daemon.ZillaRuntime.bool; import static org.cobbzilla.util.json.JsonUtil.json; import static org.cobbzilla.util.reflect.ReflectionUtil.copy; diff --git a/bubble-server/src/main/java/bubble/model/cloud/BubbleDomain.java b/bubble-server/src/main/java/bubble/model/cloud/BubbleDomain.java index 81f2ba7e..4f930f9a 100644 --- a/bubble-server/src/main/java/bubble/model/cloud/BubbleDomain.java +++ b/bubble-server/src/main/java/bubble/model/cloud/BubbleDomain.java @@ -25,8 +25,7 @@ import java.util.stream.Collectors; import static bubble.ApiConstants.EP_DOMAINS; import static bubble.model.cloud.AnsibleRole.sameRoleName; -import static org.cobbzilla.util.daemon.ZillaRuntime.die; -import static org.cobbzilla.util.daemon.ZillaRuntime.empty; +import static org.cobbzilla.util.daemon.ZillaRuntime.*; import static org.cobbzilla.util.dns.DnsType.NS; import static org.cobbzilla.util.dns.DnsType.SOA; import static org.cobbzilla.util.json.JsonUtil.json; diff --git a/bubble-server/src/main/java/bubble/model/cloud/BubbleFootprint.java b/bubble-server/src/main/java/bubble/model/cloud/BubbleFootprint.java index 28d459a2..c379bc5c 100644 --- a/bubble-server/src/main/java/bubble/model/cloud/BubbleFootprint.java +++ b/bubble-server/src/main/java/bubble/model/cloud/BubbleFootprint.java @@ -24,6 +24,7 @@ import java.util.Set; import java.util.function.Function; import static bubble.ApiConstants.EP_FOOTPRINTS; +import static org.cobbzilla.util.daemon.ZillaRuntime.bool; import static org.cobbzilla.util.json.JsonUtil.json; import static org.cobbzilla.util.reflect.ReflectionUtil.copy; import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENCRYPTED_STRING; diff --git a/bubble-server/src/main/java/bubble/notify/NewNodeNotification.java b/bubble-server/src/main/java/bubble/notify/NewNodeNotification.java index 453b4fcc..64143ff9 100644 --- a/bubble-server/src/main/java/bubble/notify/NewNodeNotification.java +++ b/bubble-server/src/main/java/bubble/notify/NewNodeNotification.java @@ -1,10 +1,16 @@ package bubble.notify; +import bubble.model.account.AccountContact; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; +import javax.persistence.Transient; + +import java.util.List; + +import static bubble.model.account.AccountContact.mask; import static java.util.UUID.randomUUID; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; @@ -31,4 +37,10 @@ public class NewNodeNotification { @Getter @Setter private String lock; + @Transient @Getter @Setter private transient AccountContact[] multifactorAuth; + + public static NewNodeNotification requiresAuth(List contacts) { + return new NewNodeNotification().setMultifactorAuth(mask(contacts)); + } + } diff --git a/bubble-server/src/main/java/bubble/resources/SessionsResource.java b/bubble-server/src/main/java/bubble/resources/SessionsResource.java deleted file mode 100644 index 9be59d65..00000000 --- a/bubble-server/src/main/java/bubble/resources/SessionsResource.java +++ /dev/null @@ -1,35 +0,0 @@ -package bubble.resources; - -import bubble.dao.SessionDAO; -import bubble.model.account.Account; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import org.cobbzilla.wizard.resources.AbstractSessionsResource; -import org.glassfish.jersey.server.ContainerRequest; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import javax.ws.rs.*; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; - -import static bubble.ApiConstants.SESSIONS_ENDPOINT; -import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; -import static org.cobbzilla.wizard.resources.ResourceUtil.*; - -@Consumes(APPLICATION_JSON) -@Produces(APPLICATION_JSON) -@Path(SESSIONS_ENDPOINT) -@Service @Slf4j -public class SessionsResource extends AbstractSessionsResource { - - @Autowired @Getter private SessionDAO sessionDAO; - - @GET - public Response me(@Context ContainerRequest ctx) { - final Account found = optionalUserPrincipal(ctx); - return ok(found); - } - -} \ No newline at end of file diff --git a/bubble-server/src/main/java/bubble/resources/account/AccountOwnedResource.java b/bubble-server/src/main/java/bubble/resources/account/AccountOwnedResource.java index 744e13cb..66628134 100644 --- a/bubble-server/src/main/java/bubble/resources/account/AccountOwnedResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/AccountOwnedResource.java @@ -120,6 +120,7 @@ public class AccountOwnedResource> data = downloadService.downloadAccountData(req, id, false); return data != null ? ok(data) : invalid("err.download.error"); } @@ -103,6 +108,8 @@ public class AccountsResource { @PathParam("id") String id, Account request) { final AccountContext c = new AccountContext(ctx, id); + authenticatorService.ensureAuthenticated(ctx, ActionTarget.account); + if (c.caller.admin()) { if (c.caller.getUuid().equals(c.account.getUuid())) { // admins cannot suspend themselves @@ -144,6 +151,7 @@ public class AccountsResource { if (policy == null) { policy = policyDAO.create(new AccountPolicy(request).setAccount(c.account.getUuid())); } else { + authenticatorService.ensureAuthenticated(ctx, policy, ActionTarget.account); policy = policyDAO.update((AccountPolicy) policy.update(request)); } return ok(policy.mask()); @@ -156,13 +164,10 @@ public class AccountsResource { @Valid AccountContact contact) { final AccountContext c = new AccountContext(ctx, id); final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); + authenticatorService.ensureAuthenticated(ctx, policy, ActionTarget.account); final AccountContact existing = policy.findContact(contact); if (existing != null) { - if (existing.isAuthenticator() && (!contact.hasUuid() || !existing.getUuid().equals(contact.getUuid()))) { - return invalid("err.authenticator.configured"); - } - // if it already exists, these fields cannot be changed contact.setUuid(existing.getUuid()); contact.setType(existing.getType()); @@ -218,6 +223,7 @@ public class AccountsResource { final AccountContext c = new AccountContext(ctx, id); if (type == CloudServiceType.authenticator) return invalid("err.info.invalid", "info should be empty for authenticator"); final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); + authenticatorService.ensureAuthenticated(ctx, policy, ActionTarget.account); final AccountContact contact = policy.findContact(new AccountContact().setType(type).setInfo(info)); if (contact == null) return notFound(type.name()+"/"+info); return ok(policyDAO.update(policy.removeContact(contact)).mask()); @@ -228,6 +234,9 @@ public class AccountsResource { @PathParam("id") String id) { final AccountContext c = new AccountContext(ctx, id); final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); + + authenticatorService.ensureAuthenticated(ctx, policy, ActionTarget.account); + final AccountContact contact = policy.findContact(new AccountContact().setType(CloudServiceType.authenticator)); if (contact == null) return notFound(CloudServiceType.authenticator.name()); return ok(policyDAO.update(policy.removeContact(contact)).mask()); @@ -238,7 +247,9 @@ public class AccountsResource { @PathParam("id") String id, @PathParam("uuid") String uuid) { final AccountContext c = new AccountContext(ctx, id); - final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); + final AccountPolicy policy = policyDAO.findSingleByAccount(c.caller.getUuid()); + authenticatorService.ensureAuthenticated(ctx, policy, ActionTarget.account); + final AccountContact found = policy.findContact(new AccountContact().setUuid(uuid)); if (found == null) return notFound(uuid); return ok(policyDAO.update(policy.removeContact(found)).mask()); @@ -250,6 +261,8 @@ public class AccountsResource { @PathParam("id") String id) { final AccountContext c = new AccountContext(ctx, id); + authenticatorService.ensureAuthenticated(ctx, ActionTarget.account); + // request deletion return ok(messageDAO.create(new AccountMessage() .setMessageType(AccountMessageType.request) @@ -268,6 +281,9 @@ public class AccountsResource { if (c.caller.getUuid().equals(c.account.getUuid())) { return invalid("err.delete.cannotDeleteSelf"); } + + authenticatorService.ensureAuthenticated(ctx, ActionTarget.account); + accountDAO.delete(c.account.getUuid()); return ok(c.account); } 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 ad8248ec..f4d679d0 100644 --- a/bubble-server/src/main/java/bubble/resources/account/AuthResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/AuthResource.java @@ -14,6 +14,7 @@ import bubble.model.cloud.BubbleNode; import bubble.model.cloud.NetworkKeys; import bubble.model.cloud.notify.NotificationReceipt; import bubble.server.BubbleConfiguration; +import bubble.service.AuthenticatorService; import bubble.service.account.StandardAccountMessageService; import bubble.service.backup.RestoreService; import bubble.service.boot.ActivationService; @@ -69,6 +70,7 @@ public class AuthResource { @Autowired private StandardAccountMessageService messageService; @Autowired private BubbleNodeDAO nodeDAO; @Autowired private BubbleConfiguration configuration; + @Autowired private AuthenticatorService authenticatorService; @GET @Path(EP_CONFIGS) public Response getPublicSystemConfigs(@Context ContainerRequest ctx) { @@ -237,9 +239,7 @@ public class AuthResource { return ok(new Account() .setName(account.getName()) .setLoginRequest(loginRequest.getUuid()) - .setMultifactorAuth(authFactors.stream() - .map(AccountContact::mask) - .toArray(AccountContact[]::new))); + .setMultifactorAuth(AccountContact.mask(authFactors))); } } } @@ -305,36 +305,42 @@ public class AuthResource { final Account caller = optionalUserPrincipal(ctx); final Account account = accountDAO.findById(request.getAccount()); if (account == null) return notFound(request.getAccount()); - if (caller != null && !caller.getUuid().equals(account.getUuid())) { - return invalid("err.token.invalid"); + if (caller != null) { + if (!caller.getUuid().equals(account.getUuid())) return invalid("err.token.invalid"); + + // authenticatorService requires the Account to have a token, or it will generate one + account.setToken(caller.getToken()); } final AccountPolicy policy = policyDAO.findSingleByAccount(account.getUuid()); final AccountContact authenticator = policy.getAuthenticator(); - if (authenticator == null) return invalid("err.authenticator.notConfigured"); - - final String secret = authenticator.totpInfo().getKey(); - final Integer code = request.intToken(); - if (code == null) return invalid("err.token.invalid"); - if (G_AUTH.authorize(secret, code)) { - if (request.verify()) { - policyDAO.update(policy.verifyContact(policy.getAuthenticator().getUuid())); - return ok_empty(); - } - final AccountMessage loginRequest = accountMessageDAO.findMostRecentLoginRequest(account.getUuid()); - final AccountMessageContact amc = messageService.accountMessageContact(loginRequest, authenticator); - final AccountMessage approval = messageService.approve(account, getRemoteHost(req), amc.key()); - if (approval.getMessageType() == AccountMessageType.confirmation) { - // OK we can log in! - return ok(account.setToken(sessionDAO.create(account))); - } else { - return ok(messageService.determineRemainingApprovals(approval)); - } + final String sessionToken = authenticatorService.authenticate(account, policy, request); + if (request.authenticate()) { + return ok_empty(); + + } else if (request.verify()) { + policyDAO.update(policy.verifyContact(policy.getAuthenticator().getUuid())); + return ok_empty(); + } + final AccountMessage loginRequest = accountMessageDAO.findMostRecentLoginRequest(account.getUuid()); + final AccountMessageContact amc = messageService.accountMessageContact(loginRequest, authenticator); + final AccountMessage approval = messageService.approve(account, getRemoteHost(req), amc.key()); + if (approval.getMessageType() == AccountMessageType.confirmation) { + // OK we can log in! + return ok(account.setToken(sessionToken)); } else { - return invalid("err.token.invalid"); + return ok(messageService.determineRemainingApprovals(approval)); } } + @DELETE @Path(EP_AUTHENTICATOR) + public Response flushAuthenticatorTokens(@Context Request req, + @Context ContainerRequest ctx) { + final Account caller = userPrincipal(ctx); + authenticatorService.flush(caller.getToken()); + return ok_empty(); + } + @POST @Path(EP_DENY+"/{token}") public Response deny(@Context Request req, @Context ContainerRequest ctx, diff --git a/bubble-server/src/main/java/bubble/resources/account/MeResource.java b/bubble-server/src/main/java/bubble/resources/account/MeResource.java index 7b76c26e..a0aa52c8 100644 --- a/bubble-server/src/main/java/bubble/resources/account/MeResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/MeResource.java @@ -7,6 +7,7 @@ import bubble.model.account.Account; import bubble.model.account.AccountPolicy; import bubble.model.account.message.AccountMessage; import bubble.model.account.message.AccountMessageType; +import bubble.model.account.message.ActionTarget; import bubble.resources.app.AppsResource; import bubble.resources.bill.AccountPaymentMethodsResource; import bubble.resources.bill.AccountPaymentsResource; @@ -17,6 +18,7 @@ import bubble.resources.driver.DriversResource; import bubble.resources.notify.ReceivedNotificationsResource; import bubble.resources.notify.SentNotificationsResource; import bubble.server.BubbleConfiguration; +import bubble.service.AuthenticatorService; import bubble.service.account.StandardAccountMessageService; import bubble.service.account.download.AccountDownloadService; import bubble.service.boot.BubbleModelSetupService; @@ -67,6 +69,7 @@ public class MeResource { @Autowired private SessionDAO sessionDAO; @Autowired private AccountDownloadService downloadService; @Autowired private BubbleConfiguration configuration; + @Autowired private AuthenticatorService authenticatorService; @GET public Response me(@Context ContainerRequest ctx) { @@ -104,6 +107,7 @@ public class MeResource { public Response changePassword(@Context ContainerRequest ctx, ChangePasswordRequest request) { final Account caller = userPrincipal(ctx); + authenticatorService.ensureAuthenticated(ctx, ActionTarget.account); if (!caller.getHashedPassword().isCorrectPassword(request.getOldPassword())) { return invalid("err.oldPassword.invalid", "old password was invalid"); } @@ -123,6 +127,7 @@ public class MeResource { @Context ContainerRequest ctx, @PathParam("token") String token) { final Account caller = userPrincipal(ctx); + authenticatorService.ensureAuthenticated(ctx, ActionTarget.account); final AccountMessage approval = messageService.approve(caller, getRemoteHost(req), token); if (approval == null) return notFound(token); @@ -138,6 +143,7 @@ public class MeResource { @Context ContainerRequest ctx, @PathParam("token") String token) { final Account caller = userPrincipal(ctx); + authenticatorService.ensureAuthenticated(ctx, ActionTarget.account); final AccountMessage denial = messageService.deny(caller, getRemoteHost(req), token); return denial != null ? ok(denial) : notFound(token); } @@ -147,6 +153,7 @@ public class MeResource { @Context ContainerRequest ctx) { final Account caller = userPrincipal(ctx); final AccountPolicy policy = policyDAO.findSingleByAccount(caller.getUuid()); + authenticatorService.ensureAuthenticated(ctx, policy, ActionTarget.account); if (policy == null || !policy.hasVerifiedAccountContacts()) { return invalid("err.download.noVerifiedContacts"); } @@ -159,6 +166,7 @@ public class MeResource { @Context ContainerRequest ctx, @PathParam("uuid") String uuid) { final Account caller = userPrincipal(ctx); + authenticatorService.ensureAuthenticated(ctx, ActionTarget.account); final JsonNode data = downloadService.retrieveAccountData(uuid); return data != null ? ok(data) : notFound(uuid); } @@ -167,6 +175,7 @@ public class MeResource { public Response runScript(@Context ContainerRequest ctx, JsonNode script) { final Account caller = userPrincipal(ctx); + authenticatorService.ensureAuthenticated(ctx); final StringWriter writer = new StringWriter(); final ApiRunnerListener listener = new ApiRunnerListenerStreamLogger("runScript", writer); @Cleanup final ApiClientBase api = configuration.newApiClient(); @@ -304,6 +313,8 @@ public class MeResource { @FormDataParam("file") InputStream in, @FormDataParam("name") String name) throws IOException { final Account caller = userPrincipal(ctx); + authenticatorService.ensureAuthenticated(ctx); + if (empty(name)) return invalid("err.name.required"); @Cleanup final TempDir temp = new TempDir(); diff --git a/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java b/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java index 980d81bf..f1adf608 100644 --- a/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java +++ b/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java @@ -2,6 +2,7 @@ package bubble.resources.bill; import bubble.cloud.CloudServiceType; import bubble.cloud.geoLocation.GeoLocation; +import bubble.dao.account.AccountPolicyDAO; import bubble.dao.account.AccountSshKeyDAO; import bubble.dao.bill.AccountPaymentMethodDAO; import bubble.dao.bill.AccountPlanDAO; @@ -21,6 +22,7 @@ import bubble.model.cloud.BubbleNetwork; import bubble.model.cloud.CloudService; import bubble.resources.account.AccountOwnedResource; import bubble.server.BubbleConfiguration; +import bubble.service.AuthenticatorService; import lombok.extern.slf4j.Slf4j; import org.cobbzilla.wizard.validation.ValidationResult; import org.glassfish.grizzly.http.server.Request; @@ -42,6 +44,7 @@ import static org.cobbzilla.wizard.resources.ResourceUtil.*; public class AccountPlansResource extends AccountOwnedResource { @Autowired private AccountSshKeyDAO sshKeyDAO; + @Autowired private AccountPolicyDAO policyDAO; @Autowired private BubbleDomainDAO domainDAO; @Autowired private BubbleNetworkDAO networkDAO; @Autowired private BubblePlanDAO planDAO; @@ -49,6 +52,7 @@ public class AccountPlansResource extends AccountOwnedResource nodes = nodeDAO.findByNetwork(network.getUuid()); if (nodes.isEmpty()) log.warn("stopNetwork: no nodes found for network: "+network.getUuid()); + authenticatorService.ensureAuthenticated(ctx, ActionTarget.network); + return ok(networkService.stopNetwork(network)); } @@ -158,6 +162,9 @@ public class NetworkActionsResource { @QueryParam("region") String region) { final Account caller = userPrincipal(ctx); if (!authAccount(caller)) return forbidden(); + + authenticatorService.ensureAuthenticated(ctx, ActionTarget.network); + return ok(networkService.restoreNetwork(network, cloud, region, req)); } @@ -170,6 +177,8 @@ public class NetworkActionsResource { final Account caller = userPrincipal(ctx); if (!caller.admin()) return forbidden(); + authenticatorService.ensureAuthenticated(ctx, ActionTarget.network); + final BubbleDomain domain = domainDAO.findByFqdn(fqdn); if (domain == null) return invalid("err.fqdn.domain.invalid", "domain not found for "+fqdn, fqdn); diff --git a/bubble-server/src/main/java/bubble/server/BubbleConfiguration.java b/bubble-server/src/main/java/bubble/server/BubbleConfiguration.java index 8ac9c9d6..4982f37f 100644 --- a/bubble-server/src/main/java/bubble/server/BubbleConfiguration.java +++ b/bubble-server/src/main/java/bubble/server/BubbleConfiguration.java @@ -41,10 +41,7 @@ import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import java.beans.Transient; import java.io.File; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Properties; +import java.util.*; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -280,4 +277,16 @@ public class BubbleConfiguration extends PgRestServerConfiguration } return false; } + + @JsonIgnore @Getter(lazy=true) private final List defaultCloudModels = initDefaultCloudModels(); + private List initDefaultCloudModels () { + final List defaults = new ArrayList<>(); + defaults.add("models/defaults/cloudService.json"); + if (paymentsEnabled()) defaults.add("models/defaults/cloudService_payment.json"); + if (testMode()) defaults.addAll(getTestCloudModels()); + return defaults; + } + + @JsonIgnore @Getter @Setter private List testCloudModels = Collections.emptyList(); + } diff --git a/bubble-server/src/main/java/bubble/service/AuthenticatorService.java b/bubble-server/src/main/java/bubble/service/AuthenticatorService.java new file mode 100644 index 00000000..6701cce6 --- /dev/null +++ b/bubble-server/src/main/java/bubble/service/AuthenticatorService.java @@ -0,0 +1,79 @@ +package bubble.service; + +import bubble.dao.SessionDAO; +import bubble.dao.account.AccountPolicyDAO; +import bubble.model.account.Account; +import bubble.model.account.AccountContact; +import bubble.model.account.AccountPolicy; +import bubble.model.account.AuthenticatorRequest; +import bubble.model.account.message.ActionTarget; +import lombok.Getter; +import org.cobbzilla.wizard.cache.redis.RedisService; +import org.glassfish.jersey.server.ContainerRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import static bubble.ApiConstants.G_AUTH; +import static org.cobbzilla.util.daemon.ZillaRuntime.now; +import static org.cobbzilla.wizard.cache.redis.RedisService.EX; +import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; +import static org.cobbzilla.wizard.resources.ResourceUtil.userPrincipal; + +@Service +public class AuthenticatorService { + + @Autowired private SessionDAO sessionDAO; + @Autowired private AccountPolicyDAO policyDAO; + @Autowired private RedisService redis; + @Getter(lazy=true) private final RedisService authenticatorTimes = redis.prefixNamespace(getClass().getSimpleName()+"_authentications"); + + public String authenticate (Account account, AccountPolicy policy, AuthenticatorRequest request) { + final AccountContact authenticator = policy.getAuthenticator(); + if (authenticator == null) throw invalidEx("err.authenticator.notConfigured"); + + final Integer code = request.intToken(); + if (code == null) throw invalidEx("err.token.invalid"); + + final String secret = authenticator.totpInfo().getKey(); + if (G_AUTH.authorize(secret, code)) { + final String sessionToken = request.startSession() ? sessionDAO.create(account) : account.getToken(); + if (sessionToken == null) throw invalidEx("err.token.noSession"); + getAuthenticatorTimes().set(sessionToken, String.valueOf(now()), EX, policy.getAuthenticatorTimeout()/1000); + return sessionToken; + + } else { + throw invalidEx("err.token.invalid"); + } + } + + public boolean isAuthenticated (String sessionToken) { return getAuthenticatorTimes().get(sessionToken) != null; } + + public void ensureAuthenticated(ContainerRequest ctx) { ensureAuthenticated(ctx, null); } + + public void ensureAuthenticated(ContainerRequest ctx, ActionTarget target) { + final Account account = userPrincipal(ctx); + final AccountPolicy policy = policyDAO.findSingleByAccount(account.getUuid()); + checkAuth(account, policy, target); + } + + public void ensureAuthenticated(ContainerRequest ctx, AccountPolicy policy, ActionTarget target) { + final Account account = userPrincipal(ctx); + checkAuth(account, policy, target); + } + + private void checkAuth(Account account, AccountPolicy policy, ActionTarget target) { + if (policy == null || !policy.hasVerifiedAuthenticator()) return; + if (target != null) { + final AccountContact authenticator = policy.getAuthenticator(); + switch (target) { + case account: if (!authenticator.requiredForAccountOperations()) return; break; + case network: if (!authenticator.requiredForNetworkOperations()) return; break; + default: throw invalidEx("err.actionTarget.invalid"); + } + } + if (!isAuthenticated(account.getToken())) throw invalidEx("err.token.invalid"); + } + + public void flush(String sessionToken) { getAuthenticatorTimes().del(sessionToken); } + +} diff --git a/bubble-server/src/main/java/bubble/service/account/StandardAccountMessageService.java b/bubble-server/src/main/java/bubble/service/account/StandardAccountMessageService.java index 0fce8862..fc8857b5 100644 --- a/bubble-server/src/main/java/bubble/service/account/StandardAccountMessageService.java +++ b/bubble-server/src/main/java/bubble/service/account/StandardAccountMessageService.java @@ -147,8 +147,6 @@ public class StandardAccountMessageService implements AccountMessageService { @Getter(lazy=true) private final AccountMessageCompletionHandler accountVerifyHandler = configuration.autowire(new AccountVerifyHandler()); @Getter(lazy=true) private final AccountMessageCompletionHandler accountDeleteHandler = configuration.autowire(new AccountDeletionHandler()); @Getter(lazy=true) private final AccountMessageCompletionHandler accountDownloadHandler = configuration.autowire(new AccountDownloadHandler()); - @Getter(lazy=true) private final AccountMessageCompletionHandler nodeStartHandler = configuration.autowire(new NodeStartHandler()); - @Getter(lazy=true) private final AccountMessageCompletionHandler nodeStopHandler = configuration.autowire(new NodeStopHandler()); @Getter(lazy=true) private final AccountMessageCompletionHandler networkPasswordHandler = configuration.autowire(new NetworkPasswordHandler()); @Getter(lazy=true) private final AccountMessageCompletionHandler networkStartHandler = configuration.autowire(new NetworkStartHandler()); @Getter(lazy=true) private final AccountMessageCompletionHandler networkStopHandler = configuration.autowire(new NetworkStopHandler()); @@ -161,8 +159,6 @@ public class StandardAccountMessageService implements AccountMessageService { handlers.put(ActionTarget.account+":"+AccountAction.verify, getAccountVerifyHandler()); handlers.put(ActionTarget.account+":"+AccountAction.delete, getAccountDeleteHandler()); handlers.put(ActionTarget.account+":"+AccountAction.download, getAccountDownloadHandler()); - handlers.put(ActionTarget.node+":"+AccountAction.start, getNodeStartHandler()); - handlers.put(ActionTarget.node+":"+AccountAction.stop, getNodeStopHandler()); handlers.put(ActionTarget.network+":"+AccountAction.password, getNetworkPasswordHandler()); handlers.put(ActionTarget.network+":"+AccountAction.start, getNetworkStartHandler()); handlers.put(ActionTarget.network+":"+AccountAction.stop, getNetworkStopHandler()); diff --git a/bubble-server/src/main/java/bubble/service/backup/BackupService.java b/bubble-server/src/main/java/bubble/service/backup/BackupService.java index cf18a17c..ee751dc9 100644 --- a/bubble-server/src/main/java/bubble/service/backup/BackupService.java +++ b/bubble-server/src/main/java/bubble/service/backup/BackupService.java @@ -130,8 +130,7 @@ public class BackupService extends SimpleDaemon { try { final String home = HOME_DIR; - final String jarPath = configuration.getEnvironment().get(ENV_BUBBLE_JAR); - final File jarFile = new File(jarPath); + final File jarFile = configuration.getBubbleJar();; if (!jarFile.exists() || jarFile.length() < 10*Bytes.MB) { return die("backup: jarFile not found or too small: "+abs(jarFile)); } diff --git a/bubble-server/src/main/java/bubble/service/boot/ActivationService.java b/bubble-server/src/main/java/bubble/service/boot/ActivationService.java index 4621fe51..c3859a53 100644 --- a/bubble-server/src/main/java/bubble/service/boot/ActivationService.java +++ b/bubble-server/src/main/java/bubble/service/boot/ActivationService.java @@ -13,6 +13,7 @@ import bubble.model.boot.ActivationRequest; import bubble.model.boot.CloudServiceConfig; import bubble.model.cloud.*; import bubble.server.BubbleConfiguration; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.cobbzilla.util.collection.ArrayUtil; @@ -36,8 +37,6 @@ import static bubble.model.cloud.BubbleFootprint.DEFAULT_FOOTPRINT_OBJECT; import static bubble.model.cloud.BubbleNetwork.TAG_ALLOW_REGISTRATION; import static bubble.model.cloud.BubbleNetwork.TAG_PARENT_ACCOUNT; import static java.util.concurrent.TimeUnit.SECONDS; -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.toMap; import static org.cobbzilla.util.daemon.ZillaRuntime.*; import static org.cobbzilla.util.io.FileUtil.toStringOrDie; import static org.cobbzilla.util.io.StreamUtil.stream2string; @@ -46,6 +45,7 @@ import static org.cobbzilla.util.network.NetworkUtil.getFirstPublicIpv4; import static org.cobbzilla.util.network.NetworkUtil.getLocalhostIpv4; import static org.cobbzilla.util.system.CommandShell.execScript; import static org.cobbzilla.util.system.Sleep.sleep; +import static org.cobbzilla.wizard.model.entityconfig.ModelSetup.scrubSpecial; import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; @Service @Slf4j @@ -252,18 +252,23 @@ public class ActivationService { @Getter(lazy=true) private final CloudService[] cloudDefaults = initCloudDefaults(); private CloudService[] initCloudDefaults() { - final CloudService[] standardServices = loadCloudServices("cloudService"); - return configuration.paymentsEnabled() - ? ArrayUtil.concat(standardServices, loadCloudServices("cloudService_payment")) - : standardServices; + CloudService[] defaults = new CloudService[0]; + for (String modelPath : configuration.getDefaultCloudModels()) { + defaults = ArrayUtil.concat(defaults, loadCloudServices(modelPath)); + } + return defaults; } - private CloudService[] loadCloudServices(final String services) { - return json(HandlebarsUtil.apply(configuration.getHandlebars(), stream2string("models/defaults/" + services + ".json"), configuration.getEnvCtx()), CloudService[].class); + private CloudService[] loadCloudServices(final String modelPath) { + final String cloudsJson = HandlebarsUtil.apply(configuration.getHandlebars(), stream2string(modelPath), configuration.getEnvCtx()); + final JsonNode cloudsArrayNode = json(cloudsJson, JsonNode.class); + return scrubSpecial(cloudsArrayNode, CloudService.class); } @Getter(lazy=true) private final Map cloudDefaultsMap = initCloudDefaultsMap(); private Map initCloudDefaultsMap() { - return Arrays.stream(getCloudDefaults()).collect(toMap(CloudService::getName, identity())); + final Map defaults = new HashMap<>(); + Arrays.stream(getCloudDefaults()).forEach(c -> defaults.put(c.getName(), c)); + return defaults; } } 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 f896f243..e3d214a2 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 @@ -106,10 +106,8 @@ message_verify_authenticator_preamble=Install the Google Authenticator app on yo message_verify_authenticator_backupCodes=Backup Codes message_verify_authenticator_backupCodes_description=If you lose your device or don't have access to it, you can use one of these backup codes. Write them down in a safe place. message_verify_authenticator_masked=Authenticator was set up elsewhere, cannot show setup/verification information here -field_label_policy_contact_requiredForNetworkUnlock=Required to unlock a new Bubble -field_label_policy_contact_requiredForNetworkUnlock_icon=fa fa-unlock -field_label_policy_contact_requiredForNodeOperations=Required for operations on your Bubble -field_label_policy_contact_requiredForNodeOperations_icon=fa fa-cloud +field_label_policy_contact_requiredForNetworkOperations=Required for operations on your Bubble +field_label_policy_contact_requiredForNetworkOperations_icon=fa fa-cloud field_label_policy_contact_requiredForAccountOperations=Required for operations on your Account field_label_policy_contact_requiredForAccountOperations_icon=fa fa-user field_label_policy_contact_receiveVerifyNotifications=Required for verification of newly added Contacts/Authorizations @@ -349,6 +347,9 @@ err.authenticator.cannotCreate=Cannot create authenticator err.authenticator.configured=Only one authenticator can be configured err.authenticator.invalid=Authenticator data is invalid err.authenticator.notConfigured=Authenticator has not been configured +err.authenticatorTimeout.required=Authenticator timeout is required +err.authenticatorTimeout.tooLong=Authenticator timeout cannot be longer than 24 hours +err.authenticatorTimeout.tooShort=Authenticator timeout cannot be shorter than 1 minute err.backup.cannotDelete=Cannot delete backup with its current status err.backupCleaner.didNotRun=Backup cleaner did not run err.backupCleaner.neverRun=Backup cleaner was never run diff --git a/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties b/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties index e7312a6c..cfd7cfdf 100644 --- a/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties +++ b/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties @@ -80,6 +80,11 @@ err.timezone.unknown=An error occurred trying to determine the time zone err.timezone.length=Time zone is too long err.timezone.required=Time zone is required +# Authenticator token errors +err.token.invalid=Code is incorrect +err.token.invalidActionTarget=Action target was invalid (expected 'account' or 'network') +err.token.noSession=Session required for authenticator + err.geoCodeService.notFound=GeoCode service not found err.geoLocateService.notFound=GeoLocation service not found @@ -188,7 +193,6 @@ field_label_policy_contact_type_authenticator=Authentication App field_label_policy_contact_verified=Verified field_label_policy_contact_verify_code=Enter Verification Code button_label_submit_verify_code=Verify -err.token.invalid=Code is incorrect # Low-level errors and activation errors err.cloud.noSuchField=A cloud driver config field name is invalid diff --git a/bubble-server/src/test/java/bubble/test/ActivatedBubbleModelTestBase.java b/bubble-server/src/test/java/bubble/test/ActivatedBubbleModelTestBase.java index 4af40834..93f82325 100644 --- a/bubble-server/src/test/java/bubble/test/ActivatedBubbleModelTestBase.java +++ b/bubble-server/src/test/java/bubble/test/ActivatedBubbleModelTestBase.java @@ -2,7 +2,7 @@ package bubble.test; import bubble.cloud.CloudServiceDriver; import bubble.cloud.CloudServiceType; -import bubble.cloud.dns.godaddy.GoDaddyDnsDriver; +import bubble.cloud.dns.mock.MockDnsDriver; import bubble.cloud.storage.local.LocalStorageDriver; import bubble.model.account.Account; import bubble.model.boot.ActivationRequest; @@ -13,6 +13,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.github.jknack.handlebars.Handlebars; import lombok.Cleanup; import lombok.extern.slf4j.Slf4j; +import org.cobbzilla.util.collection.ArrayUtil; +import org.cobbzilla.util.handlebars.HandlebarsUtil; import org.cobbzilla.wizard.api.ValidationException; import org.cobbzilla.wizard.auth.LoginRequest; import org.cobbzilla.wizard.client.ApiClientBase; @@ -20,6 +22,7 @@ import org.cobbzilla.wizard.client.script.ApiRunner; import org.cobbzilla.wizard.model.entityconfig.ModelSetupListener; import org.cobbzilla.wizard.server.RestServer; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -29,8 +32,7 @@ import java.util.stream.Collectors; import static bubble.ApiConstants.*; import static bubble.model.account.Account.ROOT_USERNAME; import static bubble.service.boot.StandardSelfNodeService.THIS_NODE_FILE; -import static org.cobbzilla.util.daemon.ZillaRuntime.die; -import static org.cobbzilla.util.daemon.ZillaRuntime.shortError; +import static org.cobbzilla.util.daemon.ZillaRuntime.*; import static org.cobbzilla.util.handlebars.HandlebarsUtil.applyReflectively; import static org.cobbzilla.util.io.FileUtil.abs; import static org.cobbzilla.util.io.StreamUtil.stream2string; @@ -54,7 +56,8 @@ public abstract class ActivatedBubbleModelTestBase extends BubbleModelTestBase { @Override public void beforeStart(RestServer server) { // if server hbm2ddl mode is validate, do not delete node file - if (server.getConfiguration().dbExists()) { + final BubbleConfiguration configuration = server.getConfiguration(); + if (configuration.dbExists()) { hasExistingDb = true; log.info("beforeStart: not deleting "+abs(THIS_NODE_FILE)+" because DB exists"); } else { @@ -63,14 +66,24 @@ public abstract class ActivatedBubbleModelTestBase extends BubbleModelTestBase { die("beforeStart: error deleting " + abs(THIS_NODE_FILE)); } } + + // set default domain + final Map env = configuration.getEnvironment(); + env.put("defaultDomain", getDefaultDomain()); + env.put("TEST_DEFAULT_DNS_CLOUD", "MockDns"); + configuration.setTestCloudModels(getCloudServiceModels()); + super.beforeStart(server); } + public String getDefaultDomain() { return "example.com"; } + @Override protected String[] getSqlPostScripts() { return hasExistingDb ? null : super.getSqlPostScripts(); } @Override protected void modelTest(final String name, ApiRunner apiRunner) throws Exception { getApi().logout(); final Account root = getApi().post(AUTH_ENDPOINT + EP_LOGIN, new LoginRequest(ROOT_USERNAME, ROOT_PASSWORD), Account.class); + if (empty(root.getToken())) die("modelTest: error logging in root user (was MFA configured in a previous test?): "+json(root)); getApi().pushToken(root.getToken()); apiRunner.addNamedSession(ROOT_SESSION, root.getToken()); apiRunner.run(include(name)); @@ -87,7 +100,10 @@ public abstract class ActivatedBubbleModelTestBase extends BubbleModelTestBase { final Map ctx = configuration.getEnvCtx(); // load all clouds - final CloudService[] clouds = scrubSpecial(json(stream2string("models/system/cloudService.json"), JsonNode.class, FULL_MAPPER_ALLOW_COMMENTS), CloudService.class); + CloudService[] clouds = new CloudService[0]; + for (String model : getCloudServiceModels()) { + clouds = ArrayUtil.concat(clouds, scrubSpecial(json(stream2string(model), JsonNode.class, FULL_MAPPER_ALLOW_COMMENTS), CloudService.class)); + } // determine domain final BubbleDomain domain = getDomain(ctx, clouds); @@ -99,7 +115,9 @@ public abstract class ActivatedBubbleModelTestBase extends BubbleModelTestBase { final CloudService storage = getNetworkStorage(ctx, clouds); // sanity check - if (!dns.getName().equals(domain.getPublicDns())) die("onStart: DNS service mismatch: domain references "+domain.getPublicDns()+" but DNS service selected has name "+dns.getName()); + if (!dns.getName().equals(domain.getPublicDns())) { + die("onStart: DNS service mismatch: domain references "+domain.getPublicDns()+" but DNS service selected has name "+dns.getName()); + } @Cleanup final ApiClientBase client = configuration.newApiClient(); @@ -132,13 +150,18 @@ public abstract class ActivatedBubbleModelTestBase extends BubbleModelTestBase { if (!hasExistingDb) super.onStart(server); } + protected List getCloudServiceModels() { + final ArrayList models = new ArrayList<>(); + models.add("models/system/cloudService.json"); + models.add("models/system/cloudService_test.json"); + return models; + } + private BubbleDomain getDomain(Map ctx, CloudService[] clouds) { final Handlebars handlebars = getConfiguration().getHandlebars(); - final BubbleDomain[] allDomains = json(stream2string("models/system/bubbleDomain.json"), BubbleDomain[].class, FULL_MAPPER_ALLOW_COMMENTS); - final BubbleDomain candidateDomain = Arrays.stream(allDomains).filter(getDomainFilter(clouds)).findFirst().orElse(null); - return candidateDomain != null - ? applyReflectively(handlebars, candidateDomain, ctx) - : die("getDomain: no candidate domain found"); + final JsonNode domainsArray = json(HandlebarsUtil.apply(handlebars, stream2string("models/system/bubbleDomain.json"), ctx), JsonNode.class, FULL_MAPPER_ALLOW_COMMENTS); + final BubbleDomain[] allDomains = scrubSpecial(domainsArray, BubbleDomain.class); + return Arrays.stream(allDomains).filter(getDomainFilter(clouds)).findFirst().orElseGet(() -> die("getDomain: no candidate domain found")); } protected Predicate getDomainFilter(CloudService[] clouds) { return bubbleDomain -> true; } @@ -157,7 +180,7 @@ public abstract class ActivatedBubbleModelTestBase extends BubbleModelTestBase { private CloudService getPublicDns(Map ctx, CloudService[] clouds) { return findByTypeAndDriver(ctx, clouds, CloudServiceType.dns, getPublicDnsDriver()); } - protected Class getPublicDnsDriver() { return GoDaddyDnsDriver.class; } + protected Class getPublicDnsDriver() { return MockDnsDriver.class; } protected CloudService getNetworkStorage(Map ctx, CloudService[] clouds) { return findByTypeAndDriver(ctx, clouds, CloudServiceType.storage, getNetworkStorageDriver()); diff --git a/bubble-server/src/test/java/bubble/test/AuthTest.java b/bubble-server/src/test/java/bubble/test/AuthTest.java index fc880740..f508f3b6 100644 --- a/bubble-server/src/test/java/bubble/test/AuthTest.java +++ b/bubble-server/src/test/java/bubble/test/AuthTest.java @@ -6,9 +6,7 @@ import org.junit.Test; @Slf4j public class AuthTest extends ActivatedBubbleModelTestBase { - private static final String MANIFEST_ALL = "manifest-all"; - - @Override protected String getManifest() { return MANIFEST_ALL; } + @Override protected String getManifest() { return "manifest-test"; } @Test public void testBasicAuth () throws Exception { modelTest("auth/basic_auth"); } @Test public void testAccountCrud () throws Exception { modelTest("auth/account_crud"); } @@ -17,5 +15,6 @@ public class AuthTest extends ActivatedBubbleModelTestBase { @Test public void testForgotPassword () throws Exception { modelTest("auth/forgot_password"); } @Test public void testMultifactorAuth () throws Exception { modelTest("auth/multifactor_auth"); } @Test public void testDownloadAccount () throws Exception { modelTest("auth/download_account"); } + @Test public void testNetworkAuth () throws Exception { modelTest("auth/network_auth"); } } diff --git a/bubble-server/src/test/java/bubble/test/BackupTest.java b/bubble-server/src/test/java/bubble/test/BackupTest.java new file mode 100644 index 00000000..95a9943b --- /dev/null +++ b/bubble-server/src/test/java/bubble/test/BackupTest.java @@ -0,0 +1,11 @@ +package bubble.test; + +import org.junit.Test; + +public class BackupTest extends NetworkTestBase { + + @Override public boolean backupsEnabled() { return true; } + + @Test public void testSimpleBackup () throws Exception { modelTest("network/simple_backup"); } + +} diff --git a/bubble-server/src/test/java/bubble/test/BubbleApiRunnerListener.java b/bubble-server/src/test/java/bubble/test/BubbleApiRunnerListener.java index 1044c6de..c80399c1 100644 --- a/bubble-server/src/test/java/bubble/test/BubbleApiRunnerListener.java +++ b/bubble-server/src/test/java/bubble/test/BubbleApiRunnerListener.java @@ -4,6 +4,7 @@ import bubble.BubbleHandlebars; import bubble.cloud.CloudServiceType; import bubble.cloud.payment.stripe.StripePaymentDriver; import bubble.dao.account.AccountDAO; +import bubble.dao.cloud.BubbleDomainDAO; import bubble.dao.cloud.CloudServiceDAO; import bubble.mock.MockStripePaymentDriver; import bubble.model.account.Account; @@ -12,6 +13,7 @@ import bubble.server.BubbleConfiguration; import bubble.service.bill.BillingService; import com.github.jknack.handlebars.Handlebars; import com.stripe.model.Token; +import lombok.extern.slf4j.Slf4j; import org.cobbzilla.util.string.StringUtil; import org.cobbzilla.wizard.client.script.SimpleApiRunnerListener; @@ -19,13 +21,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import static bubble.ApiConstants.getBubbleDefaultDomain; import static java.util.concurrent.TimeUnit.SECONDS; -import static org.cobbzilla.util.daemon.ZillaRuntime.die; -import static org.cobbzilla.util.daemon.ZillaRuntime.incrementSystemTimeOffset; +import static org.cobbzilla.util.daemon.ZillaRuntime.*; import static org.cobbzilla.util.system.Sleep.sleep; import static org.cobbzilla.util.time.TimeUtil.parseDuration; +@Slf4j public class BubbleApiRunnerListener extends SimpleApiRunnerListener { public static final String FAST_FORWARD_AND_BILL = "fast_forward_and_bill"; @@ -115,7 +116,11 @@ public class BubbleApiRunnerListener extends SimpleApiRunnerListener { @Override public void setCtxVars(Map ctx) { ctx.put("serverConfig", configuration); - ctx.put("defaultDomain", getBubbleDefaultDomain()); + try { + ctx.put("defaultDomain", configuration.getBean(BubbleDomainDAO.class).findAll().get(0).getName()); + } catch (Exception e) { + log.warn("setCtxVars: error setting defaultDomain: "+shortError(e)); + } } } diff --git a/bubble-server/src/test/java/bubble/test/BubbleCoreSuite.java b/bubble-server/src/test/java/bubble/test/BubbleCoreSuite.java index 200951fe..5b94bdd9 100644 --- a/bubble-server/src/test/java/bubble/test/BubbleCoreSuite.java +++ b/bubble-server/src/test/java/bubble/test/BubbleCoreSuite.java @@ -12,6 +12,8 @@ import org.junit.runners.Suite; DriverTest.class, ProxyTest.class, TrafficAnalyticsTest.class, - NetworkTest.class + BackupTest.class, + NetworkTest.class, + NetworkKeysTest.class }) public class BubbleCoreSuite {} diff --git a/bubble-server/src/test/java/bubble/test/LiveNetworkTest.java b/bubble-server/src/test/java/bubble/test/LiveNetworkTest.java new file mode 100644 index 00000000..fc104f97 --- /dev/null +++ b/bubble-server/src/test/java/bubble/test/LiveNetworkTest.java @@ -0,0 +1,15 @@ +package bubble.test; + +import bubble.cloud.CloudServiceDriver; +import bubble.cloud.storage.s3.S3StorageDriver; +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; + +@Slf4j +public class LiveNetworkTest extends NetworkTestBase { + + @Override protected Class getNetworkStorageDriver() { return S3StorageDriver.class; } + + //@Test public void testSimpleNetwork () throws Exception { modelTest("network/simple_network"); } + +} diff --git a/bubble-server/src/test/java/bubble/test/NetworkKeysTest.java b/bubble-server/src/test/java/bubble/test/NetworkKeysTest.java new file mode 100644 index 00000000..5b1db03e --- /dev/null +++ b/bubble-server/src/test/java/bubble/test/NetworkKeysTest.java @@ -0,0 +1,9 @@ +package bubble.test; + +import org.junit.Test; + +public class NetworkKeysTest extends NetworkTestBase { + + @Test public void testGetNetworkKeys() throws Exception { modelTest("network/network_keys"); } + +} diff --git a/bubble-server/src/test/java/bubble/test/NetworkTest.java b/bubble-server/src/test/java/bubble/test/NetworkTest.java index 9cc53c50..ea9b1fc3 100644 --- a/bubble-server/src/test/java/bubble/test/NetworkTest.java +++ b/bubble-server/src/test/java/bubble/test/NetworkTest.java @@ -1,19 +1,12 @@ package bubble.test; -import bubble.cloud.CloudServiceDriver; -import bubble.cloud.storage.s3.S3StorageDriver; import lombok.extern.slf4j.Slf4j; import org.junit.Test; @Slf4j public class NetworkTest extends NetworkTestBase { - @Override protected Class getNetworkStorageDriver() { return S3StorageDriver.class; } - @Test public void testRegions () throws Exception { modelTest("network/network_regions"); } - @Test public void testSimpleNetwork () throws Exception { modelTest("network/simple_network"); } @Test public void testUpgradeRole () throws Exception { modelTest("network/upgrade_role"); } - @Test public void testSimpleBackup () throws Exception { modelTest("network/simple_backup"); } - @Test public void testGetNetworkKeys() throws Exception { modelTest("network/network_keys"); } } diff --git a/bubble-server/src/test/java/bubble/test/NetworkTestBase.java b/bubble-server/src/test/java/bubble/test/NetworkTestBase.java index 4597df43..6bd988c2 100644 --- a/bubble-server/src/test/java/bubble/test/NetworkTestBase.java +++ b/bubble-server/src/test/java/bubble/test/NetworkTestBase.java @@ -2,8 +2,6 @@ package bubble.test; public class NetworkTestBase extends ActivatedBubbleModelTestBase { - public static final String MANIFEST_NETWORK = "manifest-network"; - - @Override protected String getManifest() { return MANIFEST_NETWORK; } + @Override protected String getManifest() { return "manifest-network"; } } diff --git a/bubble-server/src/test/java/bubble/test/PaymentTest.java b/bubble-server/src/test/java/bubble/test/PaymentTest.java index 7eed36af..ecbbfce5 100644 --- a/bubble-server/src/test/java/bubble/test/PaymentTest.java +++ b/bubble-server/src/test/java/bubble/test/PaymentTest.java @@ -10,7 +10,7 @@ import org.junit.Test; @Slf4j public class PaymentTest extends ActivatedBubbleModelTestBase { - @Override protected String getManifest() { return "manifest-payment"; } + @Override protected String getManifest() { return "manifest-test"; } @Override public void beforeStart(RestServer server) { final BubbleConfiguration configuration = server.getConfiguration(); diff --git a/bubble-server/src/test/resources/models/include/add_authenticator.json b/bubble-server/src/test/resources/models/include/add_authenticator.json new file mode 100644 index 00000000..7b50a6e6 --- /dev/null +++ b/bubble-server/src/test/resources/models/include/add_authenticator.json @@ -0,0 +1,57 @@ +[ + { + "comment": "declare default parameters for add_authenticator test part", + "include": "_defaults", + "params": { + "userId": "_required", + "authFactor": null, + "authenticatorVar": "authenticator" + } + }, + + { + "comment": "add an authenticator auth factor", + "request": { + "uri": "users/<>/policy/contacts", + "entity": { + "type": "authenticator" + } + }, + "response": { + "store": "<>", + "check": [ + {"condition": "json.getType().name() == 'authenticator'"} + ] + } + }, + + { + "comment": "verify authenticator", + "request": { + "uri": "auth/authenticator", + "entity": { + "account": "<>", + "token": "{{authenticator_token authenticator.totpKey}}", + "verify": true + } + } + }, + + { + "comment": "set authenticator auth factor to <>", + "onlyIf": "'<>' != '", + "request": { + "uri": "users/<>/policy/contacts", + "data": "authenticator", + "entity": { + "authFactor": "<>" + } + }, + "response": { + "store": "<>", + "check": [ + {"condition": "json.getType().name() == 'authenticator'"} + ] + } + } +] \ No newline at end of file diff --git a/bubble-server/src/test/resources/models/manifest-payment.json b/bubble-server/src/test/resources/models/manifest-payment.json deleted file mode 100644 index 2154bdfb..00000000 --- a/bubble-server/src/test/resources/models/manifest-payment.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - "system/cloudService", - "system/cloudService_test", - "system/bubbleDomain", - "system/bubblePlan", - "system/bubbleFootprint" -] \ No newline at end of file diff --git a/bubble-server/src/test/resources/models/manifest-test.json b/bubble-server/src/test/resources/models/manifest-test.json new file mode 100644 index 00000000..9b6b6e24 --- /dev/null +++ b/bubble-server/src/test/resources/models/manifest-test.json @@ -0,0 +1,11 @@ +[ + "system/cloudService", + "system/cloudService_test", + "system/bubbleDomain", + "system/bubblePlan", + "system/bubbleFootprint", + "system/ruleDriver", + "manifest-app-analytics", + "manifest-app-user-block-hn", + "manifest-app-user-block-localhost" +] \ No newline at end of file diff --git a/bubble-server/src/test/resources/models/system/bubbleDomain.json b/bubble-server/src/test/resources/models/system/bubbleDomain.json index e934a808..3e594c5a 100644 --- a/bubble-server/src/test/resources/models/system/bubbleDomain.json +++ b/bubble-server/src/test/resources/models/system/bubbleDomain.json @@ -1,49 +1,8 @@ [ { - "name": "bubv.net", - "publicDns": "GoDaddyDns", - "template": true, - "roles": [ - "common-0.0.1", - "firewall-0.0.1", - "bubble-0.0.1", - "algo-0.0.1", - "mitmproxy-0.0.1", - "nginx-0.0.1", - "bubble_finalizer-0.0.1" - ] - }, - { - "name": "bubblev.org", - "publicDns": "GoDaddyDns", - "template": true, - "roles": [ - "common-0.0.1", - "firewall-0.0.1", - "bubble-0.0.1", - "algo-0.0.1", - "mitmproxy-0.0.1", - "nginx-0.0.1", - "bubble_finalizer-0.0.1" - ] - }, - { - "name": "fufb.io", - "publicDns": "GoDaddyDns", - "template": true, - "roles": [ - "common-0.0.1", - "firewall-0.0.1", - "bubble-0.0.1", - "algo-0.0.1", - "mitmproxy-0.0.1", - "nginx-0.0.1", - "bubble_finalizer-0.0.1" - ] - }, - { - "name": "bubblev.net", - "publicDns": "Route53Dns", + "_subst": true, + "name": "{{defaultDomain}}", + "publicDns": "{{TEST_DEFAULT_DNS_CLOUD}}", "template": true, "roles": [ "common-0.0.1", diff --git a/bubble-server/src/test/resources/models/system/cloudService_test.json b/bubble-server/src/test/resources/models/system/cloudService_test.json index 0a3abb1d..b8c5f606 100644 --- a/bubble-server/src/test/resources/models/system/cloudService_test.json +++ b/bubble-server/src/test/resources/models/system/cloudService_test.json @@ -9,6 +9,14 @@ "template": true }, + { + "name": "MockDns", + "type": "dns", + "driverClass": "bubble.cloud.dns.mock.MockDnsDriver", + "driverConfig": {}, + "template": true + }, + { "_subst": true, "name": "FreePlay", diff --git a/bubble-server/src/test/resources/models/tests/auth/multifactor_auth.json b/bubble-server/src/test/resources/models/tests/auth/multifactor_auth.json index 830ac68b..7c373dae 100644 --- a/bubble-server/src/test/resources/models/tests/auth/multifactor_auth.json +++ b/bubble-server/src/test/resources/models/tests/auth/multifactor_auth.json @@ -182,47 +182,11 @@ }, { - "comment": "add an authenticator auth factor", - "request": { - "uri": "users/{{userAccount.name}}/policy/contacts", - "entity": { - "type": "authenticator" - } - }, - "response": { - "store": "authenticator", - "check": [ - {"condition": "json.getType().name() == 'authenticator'"} - ] - } - }, - - { - "comment": "verify authenticator", - "request": { - "uri": "auth/authenticator", - "entity": { - "account": "{{userAccount.name}}", - "token": "{{authenticator_token authenticator.totpKey}}", - "verify": true - } - } - }, - - { - "comment": "set authenticator as required auth factor", - "request": { - "uri": "users/{{userAccount.name}}/policy/contacts", - "data": "authenticator", - "entity": { - "authFactor": "required" - } - }, - "response": { - "store": "authenticator", - "check": [ - {"condition": "json.getType().name() == 'authenticator'"} - ] + "comment": "add authenticator as required auth factor", + "include": "add_authenticator", + "params": { + "userId": "{{userAccount.name}}", + "authFactor": "required" } }, diff --git a/bubble-server/src/test/resources/models/tests/auth/network_auth.json b/bubble-server/src/test/resources/models/tests/auth/network_auth.json new file mode 100644 index 00000000..9c029489 --- /dev/null +++ b/bubble-server/src/test/resources/models/tests/auth/network_auth.json @@ -0,0 +1,171 @@ +[ + { + "comment": "add email contact for root user", + "include": "add_approved_contact", + "params": { + "username": "root", + "userSession": "rootSession", + "contactInfo": "root@example.com", + "contactLookup": "root@example.com" + } + }, + + { + "comment": "add authenticator as required auth factor", + "include": "add_authenticator", + "params": { + "userId": "root", + "authFactor": "required" + } + }, + + { + "comment": "flush authenticator tokens", + "request": { + "uri": "auth/authenticator", + "method": "delete" + } + }, + + { + "comment": "add plan, fails because TOTP token not sent", + "request": { + "uri": "me/plans", + "method": "put", + "entity": { + "name": "test-net-{{rand 5}}", + "domain": "{{defaultDomain}}", + "locale": "en_US", + "timezone": "America/New_York", + "plan": "bubble", + "footprint": "US", + "paymentMethodObject": { + "paymentMethodType": "free", + "paymentInfo": "free" + } + } + }, + "response": { + "status": 422, + "check": [ + {"condition": "json.has('err.token.invalid')"} + ] + } + }, + + { + "comment": "send authenticator token", + "request": { + "uri": "auth/authenticator", + "entity": { + "account": "root", + "token": "{{authenticator_token authenticator.totpKey}}", + "authenticate": true + } + } + }, + + { + "comment": "add plan after sending TOTP token, succeeds", + "request": { + "uri": "me/plans", + "method": "put", + "entity": { + "name": "test-net-{{rand 5}}", + "domain": "{{defaultDomain}}", + "locale": "en_US", + "timezone": "America/New_York", + "plan": "bubble", + "footprint": "US", + "paymentMethodObject": { + "paymentMethodType": "free", + "paymentInfo": "free" + } + } + }, + "response": { + "store": "plan" + } + }, + + { + "comment": "flush authenticator tokens", + "request": { + "uri": "auth/authenticator", + "method": "delete" + } + }, + + { + "comment": "start the network. fails because we need a new TOTP token", + "request": { + "uri": "me/networks/{{plan.name}}/actions/start?cloud=MockCompute®ion=nyc_mock", + "method": "post" + }, + "response": { + "status": 422, + "check": [ + {"condition": "json.has('err.token.invalid')"} + ] + } + }, + + { + "comment": "update policy, do not require authenticator for network operations. fails because we need a new TOTP token", + "request": { + "uri": "users/root/policy/contacts", + "entity": { + "type": "authenticator", + "requiredForNetworkOperations": false + } + }, + "response": { + "status": 422, + "check": [ + {"condition": "json.has('err.token.invalid')"} + ] + } + }, + + { + "comment": "send authenticator token", + "request": { + "uri": "auth/authenticator", + "entity": { + "account": "root", + "token": "{{authenticator_token authenticator.totpKey}}", + "authenticate": true + } + } + }, + + { + "comment": "update policy, do not require authenticator for network operations. succeeds", + "request": { + "uri": "users/root/policy/contacts", + "entity": { + "type": "authenticator", + "requiredForNetworkOperations": false + } + } + }, + + { + "comment": "flush authenticator tokens", + "request": { + "uri": "auth/authenticator", + "method": "delete" + } + }, + + { + "comment": "start the network. succeeds without TOTP token because it is no longer required", + "request": { + "uri": "me/networks/{{plan.name}}/actions/start?cloud=MockCompute®ion=nyc_mock", + "method": "post" + }, + "response": { + "store": "nn" + } + } +] \ No newline at end of file diff --git a/utils/cobbzilla-utils b/utils/cobbzilla-utils index 39ab28e8..38e01668 160000 --- a/utils/cobbzilla-utils +++ b/utils/cobbzilla-utils @@ -1 +1 @@ -Subproject commit 39ab28e818c43b75eca844ef41fb466eb5e5a114 +Subproject commit 38e01668a1eb9a3bcb7bc29e28cfcd2daf634599 diff --git a/utils/cobbzilla-wizard b/utils/cobbzilla-wizard index 0309cd31..8f3c566b 160000 --- a/utils/cobbzilla-wizard +++ b/utils/cobbzilla-wizard @@ -1 +1 @@ -Subproject commit 0309cd313065235507e9ed20f755ff8dc34eef79 +Subproject commit 8f3c566b05bdced05af7f69d4cdf63f672f8fc61