@@ -97,6 +97,7 @@ public class ApiConstants { | |||
public static final String EP_LOGIN = "/login"; | |||
public static final String EP_APP_LOGIN = "/appLogin"; | |||
public static final String EP_LOGOUT = "/logout"; | |||
public static final String EP_TRUST = "/trust"; | |||
public static final String EP_FORGOT_PASSWORD = "/forgotPassword"; | |||
public static final String EP_CHANGE_PASSWORD = "/changePassword"; | |||
public static final String EP_ERROR_API = "/errorApi"; | |||
@@ -106,6 +107,7 @@ public class ApiConstants { | |||
public static final String EP_APPROVE = "/approve"; | |||
public static final String EP_DENY = "/deny"; | |||
public static final String EP_AUTHENTICATOR = "/authenticator"; | |||
public static final String EP_TIME = "/time"; | |||
public static final String EP_SUPPORT = "/support"; | |||
public static final String EP_APP_LINKS = "/appLinks"; | |||
public static final String EP_PATCH = "/patch"; | |||
@@ -132,7 +134,7 @@ public class ApiConstants { | |||
public static final String EP_LIST = "/list"; | |||
public static final String EP_LIST_NEXT = "/listNext"; | |||
public static final String EP_WRITE = "/write"; | |||
public static final String EP_DELETE = "/meta"; | |||
public static final String EP_DELETE = "/delete"; | |||
public static final String EP_REKEY = "/rekey"; | |||
public static final String DOMAINS_ENDPOINT = "/domains"; | |||
@@ -42,6 +42,8 @@ import java.util.Map; | |||
import java.util.concurrent.atomic.AtomicBoolean; | |||
import static bubble.ApiConstants.getRemoteHost; | |||
import static bubble.model.account.Account.ROOT_EMAIL; | |||
import static bubble.model.account.Account.ROOT_USERNAME; | |||
import static bubble.model.account.AccountTemplate.copyTemplateObjects; | |||
import static bubble.model.account.AutoUpdatePolicy.EMPTY_AUTO_UPDATE_POLICY; | |||
import static bubble.server.BubbleConfiguration.getDEFAULT_LOCALE; | |||
@@ -90,7 +92,10 @@ public class AccountDAO extends AbstractCRUDDAO<Account> implements SqlViewSearc | |||
.setPolicy(new AccountPolicy().setContact(contact, null, configuration))); | |||
} | |||
public Account findByEmail(String email) { return findByUniqueField("email", email.trim()); } | |||
public Account findByEmail(String email) { | |||
if (email.equals(ROOT_EMAIL)) email = ROOT_USERNAME; | |||
return findByUniqueField("email", email.trim()); | |||
} | |||
public Account findById(String id) { | |||
final Account found = findByUuid(id); | |||
@@ -0,0 +1,19 @@ | |||
package bubble.dao.account; | |||
import bubble.model.account.TrustedClient; | |||
import org.springframework.stereotype.Repository; | |||
import static java.util.UUID.randomUUID; | |||
@Repository | |||
public class TrustedClientDAO extends AccountOwnedEntityDAO<TrustedClient> { | |||
@Override public Object preCreate(TrustedClient trusted) { | |||
return super.preCreate(trusted.setTrustId(randomUUID().toString())); | |||
} | |||
@Override public TrustedClient postCreate(TrustedClient trusted, Object context) { | |||
return super.postCreate(trusted, context); | |||
} | |||
} |
@@ -88,6 +88,7 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci | |||
"name", "termsAgreed", "preferredPlan"); | |||
public static final String ROOT_USERNAME = "root"; | |||
public static final String ROOT_EMAIL = "root@local.local"; | |||
public static final int EMAIL_MAX_LENGTH = 100; | |||
public static Account sageMask(Account sage) { | |||
@@ -239,6 +240,7 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci | |||
@Transient public String getToken() { return getApiToken(); } | |||
public Account setToken(String token) { return setApiToken(token); } | |||
public boolean hasToken () { return !empty(getApiToken()); } | |||
public Account(Account other) { copy(this, other, CREATE_FIELDS); } | |||
@@ -0,0 +1,43 @@ | |||
package bubble.model.account; | |||
import com.fasterxml.jackson.annotation.JsonIgnore; | |||
import lombok.Getter; | |||
import lombok.NoArgsConstructor; | |||
import lombok.Setter; | |||
import lombok.experimental.Accessors; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.wizard.model.IdentifiableBase; | |||
import org.cobbzilla.wizard.model.entityconfig.annotations.*; | |||
import org.hibernate.annotations.Type; | |||
import javax.persistence.Column; | |||
import javax.persistence.Entity; | |||
import javax.persistence.Transient; | |||
import static org.cobbzilla.util.security.ShaUtil.sha256_hex; | |||
import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENCRYPTED_STRING; | |||
import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENC_PAD; | |||
@Entity @ECType(root=true) @Slf4j | |||
@NoArgsConstructor @Accessors(chain=true) | |||
@ECIndexes({@ECIndex(unique=true, of={"account", "trustId"})}) | |||
public class TrustedClient extends IdentifiableBase implements HasAccount { | |||
@ECSearchable @ECField(index=10) | |||
@ECForeignKey(entity=Account.class) | |||
@Column(nullable=false, updatable=false, length=UUID_MAXLEN) | |||
@Getter @Setter private String account; | |||
@ECField(index=20) | |||
@Type(type=ENCRYPTED_STRING) @Column(updatable=false, columnDefinition="varchar("+(100+ENC_PAD)+") NOT NULL") | |||
@JsonIgnore @Getter @Setter private String trustId; | |||
@JsonIgnore @Transient | |||
@Override public String getName() { return getTrustId(); } | |||
public boolean isValid(TrustedClientLoginRequest request) { | |||
return sha256_hex(request.getTrustSalt()+"-"+trustId).equals(request.getTrustHash()); | |||
} | |||
} |
@@ -0,0 +1,56 @@ | |||
package bubble.model.account; | |||
import com.fasterxml.jackson.annotation.JsonIgnore; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
import lombok.experimental.Accessors; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.wizard.validation.HasValue; | |||
import javax.validation.constraints.Pattern; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.shortError; | |||
import static org.cobbzilla.util.string.ValidationRegexes.UUID_REGEX; | |||
@Slf4j @Accessors(chain=true) | |||
public class TrustedClientLoginRequest { | |||
@HasValue(message="err.email.required") | |||
@Getter @Setter private String email; | |||
public boolean hasEmail () { return !empty(email); } | |||
public String getName () { return getEmail(); } | |||
public TrustedClientLoginRequest setName (String name) { return setEmail(name); } | |||
@Getter @Setter @HasValue(message="err.password.required") | |||
private String password; | |||
public boolean hasPassword () { return !empty(password); } | |||
// require timestamp to begin with a '1'. | |||
// note: this means this pattern will break on October 11, 2603 | |||
private static final String TRUST_HASH_REGEX = "^1[\\d]{10}-"+UUID_REGEX+"-"+UUID_REGEX+"$"; | |||
@HasValue(message="err.trustHash.required") | |||
@Pattern(regexp=TRUST_HASH_REGEX, message="err.trustHash.invalid") | |||
@Getter @Setter private String trustHash; | |||
private static final String TRUST_SALT_REGEX = "^"+UUID_REGEX+"$"; | |||
@HasValue(message="err.trustSalt.required") | |||
@Pattern(regexp=TRUST_SALT_REGEX, message="err.trustHash.invalid") | |||
@Getter @Setter private String trustSalt; | |||
@JsonIgnore @Getter(lazy=true) private final long time = initTime(); | |||
private long initTime () { | |||
final int firstHyphen = empty(trustSalt) ? -1 : trustSalt.indexOf('-'); | |||
if (firstHyphen <= 11) return 0; | |||
try { | |||
return Long.parseLong(trustSalt.substring(0, firstHyphen)); | |||
} catch (Exception e) { | |||
log.error("getTime: "+shortError(e)); | |||
return 0; | |||
} | |||
} | |||
} |
@@ -0,0 +1,13 @@ | |||
package bubble.model.account; | |||
import lombok.AllArgsConstructor; | |||
import lombok.Getter; | |||
import lombok.NoArgsConstructor; | |||
import lombok.Setter; | |||
@NoArgsConstructor @AllArgsConstructor | |||
public class TrustedClientResponse { | |||
@Getter @Setter private String id; | |||
} |
@@ -104,9 +104,15 @@ public class AuthResource { | |||
public Account updateLastLogin(Account account) { return accountDAO.update(account.setLastLogin()); } | |||
public String newLoginSession(Account account) { | |||
public static Account updateLastLogin(Account account, AccountDAO accountDAO) { | |||
return accountDAO.update(account.setLastLogin()); | |||
} | |||
public String newLoginSession(Account account) { return newLoginSession(account, accountDAO, sessionDAO); } | |||
public static String newLoginSession(Account account, AccountDAO accountDAO, SessionDAO sessionDAO) { | |||
if (account.getLastLogin() == null) account.setFirstLogin(true); | |||
return sessionDAO.create(updateLastLogin(account)); | |||
return sessionDAO.create(updateLastLogin(account, accountDAO)); | |||
} | |||
@GET @Path(EP_CONFIGS) | |||
@@ -352,7 +358,7 @@ public class AuthResource { | |||
// try totp token now | |||
account.setToken(authenticatorService.authenticate(account, policy, new AuthenticatorRequest() | |||
.setAccount(account.getUuid()) | |||
.setAuthenticate(true) | |||
// .setAuthenticate(true) | |||
.setToken(request.getTotpToken()))); | |||
authFactors.removeIf(AccountContact::isAuthenticator); | |||
} | |||
@@ -381,7 +387,8 @@ public class AuthResource { | |||
} | |||
} | |||
return ok(account.setToken(newLoginSession(account))); | |||
if (!account.hasToken()) account.setToken(newLoginSession(account)); | |||
return ok(account); | |||
} | |||
@POST @Path(EP_APP_LOGIN+"/{session}") | |||
@@ -440,6 +447,9 @@ public class AuthResource { | |||
} | |||
} | |||
@Path(EP_TRUST) | |||
public TrustedAuthResource getTrustedAuthResource() { return configuration.subResource(TrustedAuthResource.class); } | |||
@POST @Path(EP_VERIFY_KEY) | |||
public Response verifyNodeKey(@Context Request req, | |||
@Context ContainerRequest ctx, | |||
@@ -730,6 +740,8 @@ public class AuthResource { | |||
return ok_empty(); | |||
} | |||
@GET @Path(EP_TIME) public Response serverTime() { return ok(now()); } | |||
@Autowired private GeoService geoService; | |||
@GET @Path(EP_SUPPORT) | |||
@@ -0,0 +1,124 @@ | |||
package bubble.resources.account; | |||
import bubble.dao.SessionDAO; | |||
import bubble.dao.account.AccountDAO; | |||
import bubble.dao.account.AccountPolicyDAO; | |||
import bubble.dao.account.TrustedClientDAO; | |||
import bubble.model.account.*; | |||
import bubble.model.account.message.ActionTarget; | |||
import bubble.service.account.StandardAuthenticatorService; | |||
import lombok.Getter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.wizard.cache.redis.RedisService; | |||
import org.glassfish.jersey.server.ContainerRequest; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import javax.validation.Valid; | |||
import javax.ws.rs.*; | |||
import javax.ws.rs.core.Context; | |||
import javax.ws.rs.core.Response; | |||
import java.util.List; | |||
import static bubble.ApiConstants.EP_DELETE; | |||
import static bubble.resources.account.AuthResource.newLoginSession; | |||
import static java.util.concurrent.TimeUnit.SECONDS; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.now; | |||
import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; | |||
import static org.cobbzilla.wizard.cache.redis.RedisService.PX; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||
@Consumes(APPLICATION_JSON) | |||
@Produces(APPLICATION_JSON) | |||
@Slf4j | |||
public class TrustedAuthResource { | |||
private static final long MAX_TRUST_TIME_OFFSET = SECONDS.toMillis(20); | |||
@Autowired private AccountDAO accountDAO; | |||
@Autowired private AccountPolicyDAO policyDAO; | |||
@Autowired private SessionDAO sessionDAO; | |||
@Autowired private StandardAuthenticatorService authenticatorService; | |||
@Autowired private TrustedClientDAO trustedClientDAO; | |||
@Autowired private RedisService redis; | |||
@Getter(lazy=true) private final RedisService trustHashCache = redis.prefixNamespace("loginTrustedClient"); | |||
@PUT | |||
public Response trustClient(@Context ContainerRequest ctx, | |||
AccountLoginRequest request) { | |||
final Account caller = userPrincipal(ctx); | |||
final Account account = validateAccountLogin(request.getEmail(), request.getPassword()); | |||
if (!account.getUuid().equals(caller.getUuid())) return notFound(request.getEmail()); | |||
final AccountPolicy policy = policyDAO.findSingleByAccount(account.getUuid()); | |||
authenticatorService.ensureAuthenticated(ctx, policy, ActionTarget.account); | |||
return ok(new TrustedClientResponse(trustedClientDAO.create(new TrustedClient().setAccount(account.getUuid())).getTrustId())); | |||
} | |||
@POST | |||
public Response loginTrustedClient(@Context ContainerRequest ctx, | |||
@Valid TrustedClientLoginRequest request) { | |||
final Account account = validateTrustedCall(request); | |||
if (!request.hasEmail()) return invalid("err.email.required", "email is required"); | |||
if (!request.hasPassword()) return invalid("err.password.required", "password is required"); | |||
final Account validated = validateAccountLogin(request.getEmail(), request.getPassword()); | |||
if (!validated.getUuid().equals(account.getUuid())) return notFound(request.getEmail()); | |||
final TrustedClient trusted = findTrustedClient(account, request); | |||
log.info("loginTrustedClient: logging in trusted: "+account.getName()); | |||
return ok(account.setToken(newLoginSession(account, accountDAO, sessionDAO))); | |||
} | |||
@POST @Path(EP_DELETE) | |||
public Response removeTrustedClient(@Context ContainerRequest ctx, | |||
@Valid TrustedClientLoginRequest request) { | |||
final Account caller = userPrincipal(ctx); | |||
final Account validated = validateAccountLogin(request.getEmail(), request.getPassword()); | |||
if (!validated.getUuid().equals(caller.getUuid())) return notFound(request.getEmail()); | |||
final Account account = validateTrustedCall(request); | |||
final TrustedClient trusted = findTrustedClient(account, request); | |||
trustedClientDAO.delete(trusted.getUuid()); | |||
return ok_empty(); | |||
} | |||
private Account validateAccountLogin(String email, String password) { | |||
if (empty(email)) throw invalidEx("err.email.required", "email is required"); | |||
if (empty(password)) throw invalidEx("err.password.required", "password is required"); | |||
final Account account = accountDAO.findByEmail(email); | |||
if (account == null || account.deleted()) throw notFoundEx(email); | |||
if (!account.getHashedPassword().isCorrectPassword(password)) { | |||
throw notFoundEx(email); | |||
} | |||
if (account.suspended()) throw invalidEx("err.account.suspended"); | |||
return account; | |||
} | |||
private Account validateTrustedCall(TrustedClientLoginRequest request) { | |||
final Account account = accountDAO.findByEmail(request.getEmail()); | |||
if (account == null) throw notFoundEx(); | |||
if (Math.abs(now() - request.getTime()) > MAX_TRUST_TIME_OFFSET) { | |||
log.warn("validateTrustedCall: time in salt was too old or too new"); | |||
throw invalidEx("err.trustHash.invalid"); | |||
} | |||
if (getTrustHashCache().get(request.getTrustHash()) != null) { | |||
log.warn("validateTrustedCall: trustHash has already been used"); | |||
throw invalidEx("err.trustHash.invalid"); | |||
} | |||
getTrustHashCache().set(request.getTrustHash(), request.getTrustHash(), PX, MAX_TRUST_TIME_OFFSET*2); | |||
return account; | |||
} | |||
private TrustedClient findTrustedClient(Account account, TrustedClientLoginRequest request) { | |||
final List<TrustedClient> trustedClients = trustedClientDAO.findByAccount(account.getUuid()); | |||
final TrustedClient trusted = trustedClients.stream().filter(c -> c.isValid(request)).findFirst().orElse(null); | |||
if (trusted == null) { | |||
log.warn("findTrustedClient: no TrustedClient found for salt/hash"); | |||
throw notFoundEx(request.getTrustHash()); | |||
} | |||
return trusted; | |||
} | |||
} |
@@ -12,6 +12,7 @@ import bubble.model.account.AccountPolicy; | |||
import bubble.model.account.AuthenticatorRequest; | |||
import bubble.model.account.message.ActionTarget; | |||
import lombok.Getter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.wizard.cache.redis.RedisService; | |||
import org.glassfish.jersey.server.ContainerRequest; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
@@ -23,7 +24,7 @@ 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 | |||
@Service @Slf4j | |||
public class StandardAuthenticatorService implements AuthenticatorService { | |||
@Autowired private SessionDAO sessionDAO; | |||
@@ -76,6 +77,10 @@ public class StandardAuthenticatorService implements AuthenticatorService { | |||
if (policy == null || !policy.hasVerifiedAuthenticator()) return; | |||
if (target != null) { | |||
final AccountContact authenticator = policy.getAuthenticator(); | |||
if (authenticator == null) { | |||
log.info("ensureAuthenticated("+account.getName()+"): no authenticator configured"); | |||
return; | |||
} | |||
switch (target) { | |||
case account: if (!authenticator.requiredForAccountOperations()) return; break; | |||
case network: if (!authenticator.requiredForNetworkOperations()) return; break; | |||
@@ -126,14 +126,17 @@ public class StandardDeviceIdService implements DeviceIdService { | |||
@Override public void setDeviceSecurityLevel(Device device) { | |||
if (configuration.testMode()) return; | |||
for (String ip : findIpsByDevice(device.getUuid())) { | |||
redis.set_plaintext(REDIS_KEY_DEVICE_SECURITY_LEVEL_PREFIX+ip, device.getSecurityLevel().name()); | |||
if (log.isDebugEnabled()) log.debug("setDeviceSecurityLevel("+device.getName()+") setting "+REDIS_KEY_DEVICE_SECURITY_LEVEL_PREFIX+ip+" = "+device.getSecurityLevel().name()); | |||
redis.set_plaintext(REDIS_KEY_DEVICE_SECURITY_LEVEL_PREFIX + ip, device.getSecurityLevel().name()); | |||
for (AppSite site : siteDAO.findByAccount(device.getAccount())) { | |||
final String siteKey = REDIS_KEY_DEVICE_SITE_MAX_SECURITY_LEVEL_PREFIX + ip; | |||
if (site.hasMaxSecurityHosts()) { | |||
final String siteKey = REDIS_KEY_DEVICE_SITE_MAX_SECURITY_LEVEL_PREFIX + ip; | |||
if (site.enableMaxSecurityHosts()) { | |||
if (log.isDebugEnabled()) log.debug("setDeviceSecurityLevel("+device.getName()+") adding to "+REDIS_KEY_DEVICE_SITE_MAX_SECURITY_LEVEL_PREFIX+ip+": "+ site.getMaxSecurityHostsJson()); | |||
redis.sadd_plaintext(siteKey, site.getMaxSecurityHosts()); | |||
} else { | |||
if (log.isDebugEnabled()) log.debug("setDeviceSecurityLevel("+device.getName()+") removing from "+REDIS_KEY_DEVICE_SITE_MAX_SECURITY_LEVEL_PREFIX+ip+": "+ site.getMaxSecurityHostsJson()); | |||
redis.srem(siteKey, site.getMaxSecurityHosts()); | |||
} | |||
} | |||
@@ -12,6 +12,7 @@ import bubble.dao.device.DeviceDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.account.HasAccount; | |||
import bubble.model.account.ReferralCode; | |||
import bubble.model.account.TrustedClient; | |||
import bubble.model.account.message.AccountMessage; | |||
import bubble.model.bill.AccountPayment; | |||
import bubble.model.bill.Bill; | |||
@@ -41,7 +42,7 @@ public class FilteredEntityIterator extends EntityIterator { | |||
private static final List<Class<? extends Identifiable>> POST_COPY_ENTITIES = Arrays.asList(new Class[] { | |||
BubbleNode.class, BubbleNodeKey.class, Device.class, AccountMessage.class, | |||
ReferralCode.class, AccountPayment.class, Bill.class, Promotion.class, | |||
ReceivedNotification.class, SentNotification.class | |||
ReceivedNotification.class, SentNotification.class, TrustedClient.class | |||
}); | |||
private static boolean isPostCopyEntity(Class<? extends Identifiable> clazz) { | |||
@@ -1 +1 @@ | |||
bubble.version=Adventure 0.15.3 | |||
bubble.version=Adventure 0.15.4 |
@@ -0,0 +1,14 @@ | |||
CREATE TABLE trusted_client ( | |||
uuid character varying(100) NOT NULL, | |||
ctime bigint NOT NULL, | |||
mtime bigint NOT NULL, | |||
account character varying(100) NOT NULL, | |||
trust_id character varying(200) NOT NULL | |||
); | |||
ALTER TABLE ONLY trusted_client ADD CONSTRAINT trusted_client_pkey PRIMARY KEY (uuid); | |||
CREATE INDEX trusted_client_idx_account ON trusted_client USING btree (account); | |||
CREATE UNIQUE INDEX trusted_client_uniq_account_trust_id ON trusted_client USING btree (account, trust_id); | |||
ALTER TABLE ONLY trusted_client ADD CONSTRAINT trusted_client_fk_account FOREIGN KEY (account) REFERENCES account(uuid); |
@@ -62,6 +62,7 @@ | |||
<!-- <logger name="bubble.service.cloud.NodeLauncher" level="DEBUG" />--> | |||
<!-- <logger name="bubble.service.cloud.NodeService" level="DEBUG" />--> | |||
<!-- <logger name="bubble.service.cloud.NodeProgressMeter" level="DEBUG" />--> | |||
<!-- <logger name="bubble.service.cloud.StandardDeviceIdService" level="DEBUG" />--> | |||
<!-- <logger name="bubble.cloud.compute.vultr" level="DEBUG" />--> | |||
<logger name="bubble.resources.message" level="INFO" /> | |||
<logger name="bubble.app.analytics" level="DEBUG" /> | |||
@@ -842,6 +842,10 @@ err.suspended.cannotSuspendSelf=You cannot suspend yourself | |||
err.tag.invalid=Tag is invalid | |||
err.tagsJson.length=Too many tags | |||
err.tagString.length=Too many tags | |||
err.trustHash.required=trustHash is required | |||
err.trustHash.invalid=trustHash is not valid | |||
err.trustSalt.required=trustSalt is required | |||
err.trustSalt.invalid=trustSalt is not valid | |||
err.tgzB64.invalid.noRolesDir=No roles directory found in tgz | |||
err.tgzB64.invalid.wrongNumberOfFiles=Wrong number of files in tgz base directory | |||
err.tgzB64.invalid.missingTasksMainYml=No tasks/main.yml file found for role in tgz | |||
@@ -306,7 +306,7 @@ button_label_forgot_password=Send reset password | |||
forgot_password_login_link=Back to Login | |||
# New UI Labels for Registration | |||
register_title=Sign Up for a Bubble=The Safest Place on the Internet. | |||
register_title=Sign Up for a Bubble: The Safest Place on the Internet | |||
register_blurb=Block behavior tracking, ads and other rude behaviour. | |||
register_field_label_email=Email address (will be your username) | |||
field_label_password=Choose Password | |||
@@ -7,6 +7,7 @@ package bubble.test.live; | |||
import bubble.cloud.CloudServiceType; | |||
import bubble.cloud.storage.s3.S3StorageConfig; | |||
import bubble.dao.cloud.CloudServiceDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.cloud.CloudService; | |||
import bubble.model.cloud.RekeyRequest; | |||
import bubble.model.cloud.StorageMetadata; | |||
@@ -21,6 +22,7 @@ import org.cobbzilla.util.http.HttpResponseBean; | |||
import org.cobbzilla.util.io.FileUtil; | |||
import org.cobbzilla.util.security.CryptoUtil; | |||
import org.cobbzilla.wizard.api.NotFoundException; | |||
import org.cobbzilla.wizard.auth.LoginRequest; | |||
import org.junit.Test; | |||
import java.io.*; | |||
@@ -29,8 +31,11 @@ import java.util.List; | |||
import static bubble.ApiConstants.*; | |||
import static bubble.cloud.storage.StorageCryptStream.MIN_DISTINCT_LENGTH; | |||
import static bubble.cloud.storage.StorageCryptStream.MIN_KEY_LENGTH; | |||
import static bubble.model.account.Account.ROOT_USERNAME; | |||
import static bubble.model.cloud.CloudCredentials.PARAM_KEY; | |||
import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.die; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; | |||
import static org.cobbzilla.util.http.HttpMethods.DELETE; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
@@ -67,6 +72,11 @@ public class S3StorageTest extends NetworkTestBase { | |||
final S3StorageConfig config = json(s3cloud.getDriverConfigJson(), S3StorageConfig.class); | |||
cloudDAO.update(s3cloud.setDriverConfigJson(json(config.setListFetchSize(LIST_FETCH_SIZE)))); | |||
comment = "login, start api session"; | |||
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()); | |||
comment = "start with empty storage"; | |||
final HttpRequestBean predelete = new HttpRequestBean() | |||
.setMethod(DELETE) | |||
@@ -121,5 +121,164 @@ | |||
{"condition": "json.getName() === '{{userAccount.name}}'"} | |||
] | |||
} | |||
}, | |||
{ | |||
"comment": "login a third time with new session, TOTP still required", | |||
"request": { | |||
"session": "new", | |||
"uri": "auth/login", | |||
"entity": { | |||
"name": "{{userAccount.name}}", | |||
"password": "foobar1!" | |||
} | |||
}, | |||
"response": { | |||
"status": 422, | |||
"check": [ | |||
{"condition": "json.has('err.totpToken.required')"} | |||
] | |||
} | |||
}, | |||
{ | |||
"comment": "using previous valid session, set 'trustDevice' for this device", | |||
"request": { | |||
"session": "newLoginSession", | |||
"uri": "auth/trust", | |||
"method": "put", | |||
"entity": { | |||
"name": "{{userAccount.name}}", | |||
"password": "foobar1!" | |||
} | |||
}, | |||
"response": { | |||
"store": "trusted" | |||
} | |||
}, | |||
{ | |||
"comment": "logout of newLoginSession", | |||
"request": { "uri": "auth/logout" } | |||
}, | |||
{ | |||
"comment": "get server time", | |||
"request": { | |||
"uri": "auth/time" | |||
}, | |||
"response": { "store": "serverTime" } | |||
}, | |||
{ | |||
"comment": "login using trusted clientId, TOTP not required", | |||
"request": { | |||
"session": "new", | |||
"uri": "auth/trust", | |||
"entity": { | |||
"name": "{{userAccount.name}}", | |||
"password": "foobar1!", | |||
"trustHash": "{{sha256expr '[[serverTime]]-392f466c-cd17-11ea-bf46-0bb4a63a0769-[[trusted.id]]'}}", | |||
"trustSalt": "{{serverTime}}-392f466c-cd17-11ea-bf46-0bb4a63a0769" | |||
} | |||
}, | |||
"response": { | |||
"sessionName": "trustedSession", | |||
"session": "token", | |||
"check": [ | |||
{"condition": "json.getMultifactorAuth() === null"}, | |||
{"condition": "json.getToken() != null"} | |||
] | |||
} | |||
}, | |||
{ | |||
"comment": "read self account using trustedSession, succeeds", | |||
"request": { "uri": "me" }, | |||
"response": { | |||
"check": [ | |||
{"condition": "json.getName() === '{{userAccount.name}}'"} | |||
] | |||
} | |||
}, | |||
{ | |||
"comment": "remove trust for this device, fails because we used the same serverTime", | |||
"request": { | |||
"uri": "auth/trust/delete", | |||
"entity": { | |||
"name": "{{userAccount.name}}", | |||
"password": "foobar1!", | |||
"trustHash": "{{sha256expr '[[serverTime]]-392f466c-cd17-11ea-bf46-0bb4a63a0769-[[trusted.id]]'}}", | |||
"trustSalt": "{{serverTime}}-392f466c-cd17-11ea-bf46-0bb4a63a0769" | |||
} | |||
}, | |||
"response": { | |||
"status": 422, | |||
"check": [ | |||
{"condition": "json.has('err.trustHash.invalid')"} | |||
] | |||
} | |||
}, | |||
{ | |||
"comment": "get updated server time", | |||
"request": { | |||
"uri": "auth/time" | |||
}, | |||
"response": { "store": "serverTime" } | |||
}, | |||
{ | |||
"comment": "remove trust for this device, succeeds", | |||
"request": { | |||
"uri": "auth/trust/delete", | |||
"entity": { | |||
"name": "{{userAccount.name}}", | |||
"password": "foobar1!", | |||
"trustHash": "{{sha256expr '[[serverTime]]-392f466c-cd17-11ea-bf46-0bb4a63a0769-[[trusted.id]]'}}", | |||
"trustSalt": "{{serverTime}}-392f466c-cd17-11ea-bf46-0bb4a63a0769" | |||
} | |||
} | |||
}, | |||
{ | |||
"comment": "login a fifth time with new session, TOTP required again", | |||
"request": { | |||
"session": "new", | |||
"uri": "auth/login", | |||
"entity": { | |||
"name": "{{userAccount.name}}", | |||
"password": "foobar1!" | |||
} | |||
}, | |||
"response": { | |||
"status": 422, | |||
"check": [ | |||
{"condition": "json.has('err.totpToken.required')"} | |||
] | |||
} | |||
}, | |||
{ | |||
"comment": "login 5th session with TOTP token, succeeds", | |||
"request": { | |||
"session": "new", | |||
"uri": "auth/login", | |||
"entity": { | |||
"name": "{{userAccount.name}}", | |||
"password": "foobar1!", | |||
"totpToken": "{{authenticator_token authenticator.totpKey}}" | |||
} | |||
}, | |||
"response": { | |||
"sessionName": "newLoginSession", | |||
"session": "token", | |||
"check": [ | |||
{"condition": "json.getMultifactorAuth() === null"}, | |||
{"condition": "json.getToken() != null"} | |||
] | |||
} | |||
} | |||
] |
@@ -1 +1 @@ | |||
Subproject commit 74117dccae90c8905c8f4f54d16fcf19ce5eb27e | |||
Subproject commit 55cc17c6c4241b8eb6c1ada2ce29ce08307bbfee |
@@ -1 +1 @@ | |||
Subproject commit a39467dbcd062ed1471e44450d495ba7a9bd8942 | |||
Subproject commit fc87156268ff3380321b83d9b6e2408441f58047 |
@@ -1 +1 @@ | |||
Subproject commit 5510f2d6563ed6109ce153e49acd03ead6a6f4cd | |||
Subproject commit 360ea8067406a3babdf8cd6488a4f5c391ac36bf |