@@ -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"; | |||
@@ -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; | |||
} | |||
} | |||
@@ -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()); | |||
} | |||
} |
@@ -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; | |||
@@ -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); | |||
} | |||
@@ -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) { | |||
@@ -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; | |||
} | |||
@@ -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(); } | |||
} |
@@ -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; | |||
@@ -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); } | |||
@@ -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; | |||
@@ -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; | |||
@@ -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; | |||
@@ -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; | |||
@@ -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; | |||
@@ -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)); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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)); | |||
} | |||
@@ -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); | |||
} | |||
@@ -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, | |||
@@ -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(); | |||
@@ -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(); | |||
@@ -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); | |||
@@ -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(); | |||
} |
@@ -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); } | |||
} |
@@ -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()); | |||
@@ -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)); | |||
} | |||
@@ -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; | |||
} | |||
} |
@@ -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 | |||
@@ -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 | |||
@@ -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()); | |||
@@ -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"); } | |||
} |
@@ -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"); } | |||
} |
@@ -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)); | |||
} | |||
} | |||
} |
@@ -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 {} |
@@ -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"); } | |||
} |
@@ -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"); } | |||
} |
@@ -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"); } | |||
} |
@@ -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"; } | |||
} |
@@ -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(); | |||
@@ -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'"} | |||
] | |||
} | |||
} | |||
] |
@@ -1,7 +0,0 @@ | |||
[ | |||
"system/cloudService", | |||
"system/cloudService_test", | |||
"system/bubbleDomain", | |||
"system/bubblePlan", | |||
"system/bubbleFootprint" | |||
] |
@@ -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" | |||
] |
@@ -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", | |||
@@ -9,6 +9,14 @@ | |||
"template": true | |||
}, | |||
{ | |||
"name": "MockDns", | |||
"type": "dns", | |||
"driverClass": "bubble.cloud.dns.mock.MockDnsDriver", | |||
"driverConfig": {}, | |||
"template": true | |||
}, | |||
{ | |||
"_subst": true, | |||
"name": "FreePlay", | |||
@@ -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" | |||
} | |||
}, | |||
@@ -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" | |||
} | |||
} | |||
] |
@@ -1 +1 @@ | |||
Subproject commit 39ab28e818c43b75eca844ef41fb466eb5e5a114 | |||
Subproject commit 38e01668a1eb9a3bcb7bc29e28cfcd2daf634599 |
@@ -1 +1 @@ | |||
Subproject commit 0309cd313065235507e9ed20f755ff8dc34eef79 | |||
Subproject commit 8f3c566b05bdced05af7f69d4cdf63f672f8fc61 |