Browse Source

use authenticator. refactor tests to use example domain and mock dns

tags/v0.1.7
Jonathan Cobb 4 years ago
parent
commit
7cba082bc0
49 changed files with 679 additions and 256 deletions
  1. +9
    -4
      bubble-server/src/main/java/bubble/ApiConstants.java
  2. +0
    -2
      bubble-server/src/main/java/bubble/cloud/auth/AuthenticationDriver.java
  3. +37
    -0
      bubble-server/src/main/java/bubble/cloud/dns/mock/MockDnsDriver.java
  4. +1
    -1
      bubble-server/src/main/java/bubble/cloud/storage/StorageServiceDriver.java
  5. +3
    -2
      bubble-server/src/main/java/bubble/main/BubbleScriptMain.java
  6. +8
    -15
      bubble-server/src/main/java/bubble/model/account/AccountContact.java
  7. +43
    -9
      bubble-server/src/main/java/bubble/model/account/AccountPolicy.java
  8. +5
    -0
      bubble-server/src/main/java/bubble/model/account/AuthenticatorRequest.java
  9. +1
    -1
      bubble-server/src/main/java/bubble/model/account/message/AccountMessage.java
  10. +1
    -1
      bubble-server/src/main/java/bubble/model/account/message/ActionTarget.java
  11. +1
    -0
      bubble-server/src/main/java/bubble/model/app/BubbleApp.java
  12. +1
    -0
      bubble-server/src/main/java/bubble/model/app/RuleDriver.java
  13. +1
    -0
      bubble-server/src/main/java/bubble/model/cloud/AnsibleRole.java
  14. +1
    -2
      bubble-server/src/main/java/bubble/model/cloud/BubbleDomain.java
  15. +1
    -0
      bubble-server/src/main/java/bubble/model/cloud/BubbleFootprint.java
  16. +12
    -0
      bubble-server/src/main/java/bubble/notify/NewNodeNotification.java
  17. +0
    -35
      bubble-server/src/main/java/bubble/resources/SessionsResource.java
  18. +1
    -0
      bubble-server/src/main/java/bubble/resources/account/AccountOwnedResource.java
  19. +23
    -7
      bubble-server/src/main/java/bubble/resources/account/AccountsResource.java
  20. +31
    -25
      bubble-server/src/main/java/bubble/resources/account/AuthResource.java
  21. +11
    -0
      bubble-server/src/main/java/bubble/resources/account/MeResource.java
  22. +16
    -0
      bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java
  23. +12
    -3
      bubble-server/src/main/java/bubble/resources/cloud/NetworkActionsResource.java
  24. +13
    -4
      bubble-server/src/main/java/bubble/server/BubbleConfiguration.java
  25. +79
    -0
      bubble-server/src/main/java/bubble/service/AuthenticatorService.java
  26. +0
    -4
      bubble-server/src/main/java/bubble/service/account/StandardAccountMessageService.java
  27. +1
    -2
      bubble-server/src/main/java/bubble/service/backup/BackupService.java
  28. +14
    -9
      bubble-server/src/main/java/bubble/service/boot/ActivationService.java
  29. +5
    -4
      bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties
  30. +5
    -1
      bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties
  31. +35
    -12
      bubble-server/src/test/java/bubble/test/ActivatedBubbleModelTestBase.java
  32. +2
    -3
      bubble-server/src/test/java/bubble/test/AuthTest.java
  33. +11
    -0
      bubble-server/src/test/java/bubble/test/BackupTest.java
  34. +9
    -4
      bubble-server/src/test/java/bubble/test/BubbleApiRunnerListener.java
  35. +3
    -1
      bubble-server/src/test/java/bubble/test/BubbleCoreSuite.java
  36. +15
    -0
      bubble-server/src/test/java/bubble/test/LiveNetworkTest.java
  37. +9
    -0
      bubble-server/src/test/java/bubble/test/NetworkKeysTest.java
  38. +0
    -7
      bubble-server/src/test/java/bubble/test/NetworkTest.java
  39. +1
    -3
      bubble-server/src/test/java/bubble/test/NetworkTestBase.java
  40. +1
    -1
      bubble-server/src/test/java/bubble/test/PaymentTest.java
  41. +57
    -0
      bubble-server/src/test/resources/models/include/add_authenticator.json
  42. +0
    -7
      bubble-server/src/test/resources/models/manifest-payment.json
  43. +11
    -0
      bubble-server/src/test/resources/models/manifest-test.json
  44. +3
    -44
      bubble-server/src/test/resources/models/system/bubbleDomain.json
  45. +8
    -0
      bubble-server/src/test/resources/models/system/cloudService_test.json
  46. +5
    -41
      bubble-server/src/test/resources/models/tests/auth/multifactor_auth.json
  47. +171
    -0
      bubble-server/src/test/resources/models/tests/auth/network_auth.json
  48. +1
    -1
      utils/cobbzilla-utils
  49. +1
    -1
      utils/cobbzilla-wizard

+ 9
- 4
bubble-server/src/main/java/bubble/ApiConstants.java View File

@@ -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<String> 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";



+ 0
- 2
bubble-server/src/main/java/bubble/cloud/auth/AuthenticationDriver.java View File

@@ -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;
}
}


+ 37
- 0
bubble-server/src/main/java/bubble/cloud/dns/mock/MockDnsDriver.java View File

@@ -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<CloudApiConfig> {

private Map<String, DnsRecord> records = new ConcurrentHashMap<>();

private String recordKey(DnsRecord record) { return record.getType()+":"+record.getFqdn(); }

@Override public Collection<DnsRecord> 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<DnsRecord> list(DnsRecordMatch matcher) {
return records.values().stream().filter(matcher::matches).collect(Collectors.toList());
}

}

+ 1
- 1
bubble-server/src/main/java/bubble/cloud/storage/StorageServiceDriver.java View File

@@ -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;


+ 3
- 2
bubble-server/src/main/java/bubble/main/BubbleScriptMain.java View File

@@ -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<BubbleScriptOptions> {

public static final BubbleConfiguration DEFAULT_BUBBLE_CONFIG = new BubbleConfiguration();
@@ -12,7 +13,7 @@ public class BubbleScriptMain extends BubbleScriptMainBase<BubbleScriptOptions>
public static void main (String[] args) { main(BubbleScriptMain.class, args); }

@Override protected void setScriptContextVars(Map<String, Object> ctx) {
ctx.put("defaultDomain", ApiConstants.getBubbleDefaultDomain());
ctx.put("defaultDomain", getBubbleDefaultDomain());
ctx.put("serverConfig", DEFAULT_BUBBLE_CONFIG);
super.setScriptContextVars(ctx);
}


+ 8
- 15
bubble-server/src/main/java/bubble/model/account/AccountContact.java View File

@@ -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<AccountContact> mask(Collection<AccountContact> contacts) {
return empty(contacts) ? contacts : contacts.stream().map(c -> c.mask()).collect(Collectors.toList());
public static AccountContact[] mask(List<AccountContact> contacts) {
return contacts.stream()
.map(AccountContact::mask)
.toArray(AccountContact[]::new);
}

public ValidationResult validate(ValidationResult errors) {


+ 43
- 9
bubble-server/src/main/java/bubble/model/account/AccountPolicy.java View File

@@ -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<AccountContact> getAccountAuthFactors() {
if (!hasAccountContacts()) return Collections.emptyList();
return Arrays.stream(getAccountContacts())
.filter(AccountContact::authFactor)
.filter(AccountContact::requiredForAccountOperations)
.collect(Collectors.toList());
}

@Transient @JsonIgnore public List<AccountContact> 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;
}



+ 5
- 0
bubble-server/src/main/java/bubble/model/account/AuthenticatorRequest.java View File

@@ -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(); }

}

+ 1
- 1
bubble-server/src/main/java/bubble/model/account/message/AccountMessage.java View File

@@ -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;


+ 1
- 1
bubble-server/src/main/java/bubble/model/account/message/ActionTarget.java View File

@@ -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); }



+ 1
- 0
bubble-server/src/main/java/bubble/model/app/BubbleApp.java View File

@@ -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;


+ 1
- 0
bubble-server/src/main/java/bubble/model/app/RuleDriver.java View File

@@ -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;


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

@@ -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;



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

@@ -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;


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

@@ -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;


+ 12
- 0
bubble-server/src/main/java/bubble/notify/NewNodeNotification.java View File

@@ -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<AccountContact> contacts) {
return new NewNodeNotification().setMultifactorAuth(mask(contacts));
}

}

+ 0
- 35
bubble-server/src/main/java/bubble/resources/SessionsResource.java View File

@@ -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<Account> {

@Autowired @Getter private SessionDAO sessionDAO;

@GET
public Response me(@Context ContainerRequest ctx) {
final Account found = optionalUserPrincipal(ctx);
return ok(found);
}

}

+ 1
- 0
bubble-server/src/main/java/bubble/resources/account/AccountOwnedResource.java View File

@@ -120,6 +120,7 @@ public class AccountOwnedResource<E extends HasAccount, DAO extends AccountOwned
}
if (found != null) {
if (!canUpdate(ctx, caller, found, request)) return ok(found);
setReferences(ctx, caller, request);
found.update(request);
return ok(getDao().update(found));
}


+ 23
- 7
bubble-server/src/main/java/bubble/resources/account/AccountsResource.java View File

@@ -22,6 +22,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.download.AccountDownloadService;
import bubble.service.cloud.StandardNetworkService;
import lombok.extern.slf4j.Slf4j;
@@ -54,6 +55,7 @@ public class AccountsResource {
@Autowired private AccountPolicyDAO policyDAO;
@Autowired private AccountMessageDAO messageDAO;
@Autowired private AccountDownloadService downloadService;
@Autowired private AuthenticatorService authenticatorService;

@GET
public Response list(@Context ContainerRequest ctx) {
@@ -88,12 +90,15 @@ public class AccountsResource {
return ok(created.waitForAccountInit());
}

@GET @Path("/{id}"+EP_DOWNLOAD)
@POST @Path("/{id}"+EP_DOWNLOAD)
public Response downloadAllUserData(@Context Request req,
@Context ContainerRequest ctx,
@PathParam("id") String id) {
final AccountContext c = new AccountContext(ctx, id);
if (!c.account.admin()) return forbidden();
if (!c.caller.admin()) return forbidden();

authenticatorService.ensureAuthenticated(ctx, ActionTarget.account);

final Map<String, List<String>> 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);
}


+ 31
- 25
bubble-server/src/main/java/bubble/resources/account/AuthResource.java View File

@@ -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,


+ 11
- 0
bubble-server/src/main/java/bubble/resources/account/MeResource.java View File

@@ -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();


+ 16
- 0
bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java View File

@@ -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<AccountPlan, AccountPlanDAO> {

@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<AccountPlan, Acco
@Autowired private CloudServiceDAO cloudDAO;
@Autowired private AccountPaymentMethodDAO paymentMethodDAO;
@Autowired private BubbleConfiguration configuration;
@Autowired private AuthenticatorService authenticatorService;

public AccountPlansResource(Account account) { super(account); }

@@ -64,6 +68,8 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco
}

@Override protected boolean canCreate(Request req, ContainerRequest ctx, Account caller, AccountPlan request) {
authenticatorService.ensureAuthenticated(ctx);

// ensure caller is not from a disallowed country
if (configuration.hasDisallowedCountries()) {
// do we have a geoLocation service?
@@ -82,6 +88,16 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco
return super.canCreate(req, ctx, caller, request);
}

@Override protected boolean canUpdate(ContainerRequest ctx, Account caller, AccountPlan found, AccountPlan request) {
authenticatorService.ensureAuthenticated(ctx);
return super.canUpdate(ctx, caller, found, request);
}

@Override protected boolean canDelete(ContainerRequest ctx, Account caller, AccountPlan found) {
authenticatorService.ensureAuthenticated(ctx);
return super.canDelete(ctx, caller, found);
}

@Override protected AccountPlan setReferences(ContainerRequest ctx, Account caller, AccountPlan request) {

final ValidationResult errors = new ValidationResult();


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

@@ -15,6 +15,7 @@ import bubble.model.account.message.ActionTarget;
import bubble.model.bill.AccountPlan;
import bubble.model.cloud.*;
import bubble.server.BubbleConfiguration;
import bubble.service.AuthenticatorService;
import bubble.service.backup.NetworkKeysService;
import bubble.service.cloud.NodeProgressMeterTick;
import bubble.service.cloud.StandardNetworkService;
@@ -50,6 +51,7 @@ public class NetworkActionsResource {
@Autowired private BubbleNetworkDAO networkDAO;
@Autowired private AccountPlanDAO accountPlanDAO;
@Autowired private BubbleConfiguration configuration;
@Autowired private AuthenticatorService authenticatorService;

private Account account;
private BubbleNetwork network;
@@ -77,9 +79,7 @@ public class NetworkActionsResource {
}

if (!network.getState().canStartNetwork()) return invalid("err.network.cannotStartInCurrentState");

final AccountPolicy policy = policyDAO.findSingleByAccount(account.getUuid());
// todo: enforce policy
authenticatorService.ensureAuthenticated(ctx, ActionTarget.network);

return _startNetwork(network, cloud, region, req);
}
@@ -129,6 +129,8 @@ public class NetworkActionsResource {
final Account caller = userPrincipal(ctx);
if (!caller.admin()) return forbidden();

authenticatorService.ensureAuthenticated(ctx, ActionTarget.network);

final String encryptionKey = enc == null ? null : enc.getValue();
final ConstraintViolationBean error = validatePassword(encryptionKey);
if (error != null) return invalid(error);
@@ -148,6 +150,8 @@ public class NetworkActionsResource {
final List<BubbleNode> 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);



+ 13
- 4
bubble-server/src/main/java/bubble/server/BubbleConfiguration.java View File

@@ -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<String> defaultCloudModels = initDefaultCloudModels();
private List<String> initDefaultCloudModels () {
final List<String> 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<String> testCloudModels = Collections.emptyList();

}

+ 79
- 0
bubble-server/src/main/java/bubble/service/AuthenticatorService.java View File

@@ -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); }

}

+ 0
- 4
bubble-server/src/main/java/bubble/service/account/StandardAccountMessageService.java View File

@@ -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());


+ 1
- 2
bubble-server/src/main/java/bubble/service/backup/BackupService.java View File

@@ -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));
}


+ 14
- 9
bubble-server/src/main/java/bubble/service/boot/ActivationService.java View File

@@ -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<String, CloudService> cloudDefaultsMap = initCloudDefaultsMap();
private Map<String, CloudService> initCloudDefaultsMap() {
return Arrays.stream(getCloudDefaults()).collect(toMap(CloudService::getName, identity()));
final Map<String, CloudService> defaults = new HashMap<>();
Arrays.stream(getCloudDefaults()).forEach(c -> defaults.put(c.getName(), c));
return defaults;
}
}

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

@@ -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


+ 5
- 1
bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties View File

@@ -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


+ 35
- 12
bubble-server/src/test/java/bubble/test/ActivatedBubbleModelTestBase.java View File

@@ -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<BubbleConfiguration> 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<String, String> 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<String, Object> 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<String> getCloudServiceModels() {
final ArrayList<String> models = new ArrayList<>();
models.add("models/system/cloudService.json");
models.add("models/system/cloudService_test.json");
return models;
}

private BubbleDomain getDomain(Map<String, Object> 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<? super BubbleDomain> getDomainFilter(CloudService[] clouds) { return bubbleDomain -> true; }
@@ -157,7 +180,7 @@ public abstract class ActivatedBubbleModelTestBase extends BubbleModelTestBase {
private CloudService getPublicDns(Map<String, Object> ctx, CloudService[] clouds) {
return findByTypeAndDriver(ctx, clouds, CloudServiceType.dns, getPublicDnsDriver());
}
protected Class<? extends CloudServiceDriver> getPublicDnsDriver() { return GoDaddyDnsDriver.class; }
protected Class<? extends CloudServiceDriver> getPublicDnsDriver() { return MockDnsDriver.class; }

protected CloudService getNetworkStorage(Map<String, Object> ctx, CloudService[] clouds) {
return findByTypeAndDriver(ctx, clouds, CloudServiceType.storage, getNetworkStorageDriver());


+ 2
- 3
bubble-server/src/test/java/bubble/test/AuthTest.java View File

@@ -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"); }

}

+ 11
- 0
bubble-server/src/test/java/bubble/test/BackupTest.java View File

@@ -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"); }

}

+ 9
- 4
bubble-server/src/test/java/bubble/test/BubbleApiRunnerListener.java View File

@@ -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<String, Object> 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));
}
}

}

+ 3
- 1
bubble-server/src/test/java/bubble/test/BubbleCoreSuite.java View File

@@ -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 {}

+ 15
- 0
bubble-server/src/test/java/bubble/test/LiveNetworkTest.java View File

@@ -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<? extends CloudServiceDriver> getNetworkStorageDriver() { return S3StorageDriver.class; }

//@Test public void testSimpleNetwork () throws Exception { modelTest("network/simple_network"); }

}

+ 9
- 0
bubble-server/src/test/java/bubble/test/NetworkKeysTest.java View File

@@ -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"); }

}

+ 0
- 7
bubble-server/src/test/java/bubble/test/NetworkTest.java View File

@@ -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<? extends CloudServiceDriver> 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"); }

}

+ 1
- 3
bubble-server/src/test/java/bubble/test/NetworkTestBase.java View File

@@ -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"; }

}

+ 1
- 1
bubble-server/src/test/java/bubble/test/PaymentTest.java View File

@@ -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<BubbleConfiguration> server) {
final BubbleConfiguration configuration = server.getConfiguration();


+ 57
- 0
bubble-server/src/test/resources/models/include/add_authenticator.json View File

@@ -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/<<userId>>/policy/contacts",
"entity": {
"type": "authenticator"
}
},
"response": {
"store": "<<authenticatorVar>>",
"check": [
{"condition": "json.getType().name() == 'authenticator'"}
]
}
},

{
"comment": "verify authenticator",
"request": {
"uri": "auth/authenticator",
"entity": {
"account": "<<userId>>",
"token": "{{authenticator_token authenticator.totpKey}}",
"verify": true
}
}
},

{
"comment": "set authenticator auth factor to <<authFactor>>",
"onlyIf": "'<<authFactor>>' != '",
"request": {
"uri": "users/<<userId>>/policy/contacts",
"data": "authenticator",
"entity": {
"authFactor": "<<authFactor>>"
}
},
"response": {
"store": "<<authenticatorVar>>",
"check": [
{"condition": "json.getType().name() == 'authenticator'"}
]
}
}
]

+ 0
- 7
bubble-server/src/test/resources/models/manifest-payment.json View File

@@ -1,7 +0,0 @@
[
"system/cloudService",
"system/cloudService_test",
"system/bubbleDomain",
"system/bubblePlan",
"system/bubbleFootprint"
]

+ 11
- 0
bubble-server/src/test/resources/models/manifest-test.json View File

@@ -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"
]

+ 3
- 44
bubble-server/src/test/resources/models/system/bubbleDomain.json View File

@@ -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",


+ 8
- 0
bubble-server/src/test/resources/models/system/cloudService_test.json View File

@@ -9,6 +9,14 @@
"template": true
},

{
"name": "MockDns",
"type": "dns",
"driverClass": "bubble.cloud.dns.mock.MockDnsDriver",
"driverConfig": {},
"template": true
},

{
"_subst": true,
"name": "FreePlay",


+ 5
- 41
bubble-server/src/test/resources/models/tests/auth/multifactor_auth.json View File

@@ -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"
}
},



+ 171
- 0
bubble-server/src/test/resources/models/tests/auth/network_auth.json View File

@@ -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&region=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&region=nyc_mock",
"method": "post"
},
"response": {
"store": "nn"
}
}
]

+ 1
- 1
utils/cobbzilla-utils

@@ -1 +1 @@
Subproject commit 39ab28e818c43b75eca844ef41fb466eb5e5a114
Subproject commit 38e01668a1eb9a3bcb7bc29e28cfcd2daf634599

+ 1
- 1
utils/cobbzilla-wizard

@@ -1 +1 @@
Subproject commit 0309cd313065235507e9ed20f755ff8dc34eef79
Subproject commit 8f3c566b05bdced05af7f69d4cdf63f672f8fc61

Loading…
Cancel
Save