Browse Source

Merge branch 'master' into kris/log_flag

pull/33/head
Kristijan Mitrovic 4 years ago
parent
commit
1538f280e9
22 changed files with 488 additions and 15 deletions
  1. +3
    -1
      bubble-server/src/main/java/bubble/ApiConstants.java
  2. +6
    -1
      bubble-server/src/main/java/bubble/dao/account/AccountDAO.java
  3. +19
    -0
      bubble-server/src/main/java/bubble/dao/account/TrustedClientDAO.java
  4. +2
    -0
      bubble-server/src/main/java/bubble/model/account/Account.java
  5. +43
    -0
      bubble-server/src/main/java/bubble/model/account/TrustedClient.java
  6. +56
    -0
      bubble-server/src/main/java/bubble/model/account/TrustedClientLoginRequest.java
  7. +13
    -0
      bubble-server/src/main/java/bubble/model/account/TrustedClientResponse.java
  8. +16
    -4
      bubble-server/src/main/java/bubble/resources/account/AuthResource.java
  9. +124
    -0
      bubble-server/src/main/java/bubble/resources/account/TrustedAuthResource.java
  10. +6
    -1
      bubble-server/src/main/java/bubble/service/account/StandardAuthenticatorService.java
  11. +5
    -2
      bubble-server/src/main/java/bubble/service/cloud/StandardDeviceIdService.java
  12. +2
    -1
      bubble-server/src/main/java/bubble/service/dbfilter/FilteredEntityIterator.java
  13. +1
    -1
      bubble-server/src/main/resources/META-INF/bubble/bubble.properties
  14. +14
    -0
      bubble-server/src/main/resources/db/migration/V2020072401__add_trusted_client.sql
  15. +1
    -0
      bubble-server/src/main/resources/logback.xml
  16. +4
    -0
      bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties
  17. +1
    -1
      bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties
  18. +10
    -0
      bubble-server/src/test/java/bubble/test/live/S3StorageTest.java
  19. +159
    -0
      bubble-server/src/test/resources/models/tests/auth/totp_auth.json
  20. +1
    -1
      bubble-web
  21. +1
    -1
      utils/cobbzilla-utils
  22. +1
    -1
      utils/cobbzilla-wizard

+ 3
- 1
bubble-server/src/main/java/bubble/ApiConstants.java View File

@@ -97,6 +97,7 @@ public class ApiConstants {
public static final String EP_LOGIN = "/login"; public static final String EP_LOGIN = "/login";
public static final String EP_APP_LOGIN = "/appLogin"; public static final String EP_APP_LOGIN = "/appLogin";
public static final String EP_LOGOUT = "/logout"; 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_FORGOT_PASSWORD = "/forgotPassword";
public static final String EP_CHANGE_PASSWORD = "/changePassword"; public static final String EP_CHANGE_PASSWORD = "/changePassword";
public static final String EP_ERROR_API = "/errorApi"; 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_APPROVE = "/approve";
public static final String EP_DENY = "/deny"; public static final String EP_DENY = "/deny";
public static final String EP_AUTHENTICATOR = "/authenticator"; 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_SUPPORT = "/support";
public static final String EP_APP_LINKS = "/appLinks"; public static final String EP_APP_LINKS = "/appLinks";
public static final String EP_PATCH = "/patch"; 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 = "/list";
public static final String EP_LIST_NEXT = "/listNext"; public static final String EP_LIST_NEXT = "/listNext";
public static final String EP_WRITE = "/write"; 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 EP_REKEY = "/rekey";


public static final String DOMAINS_ENDPOINT = "/domains"; public static final String DOMAINS_ENDPOINT = "/domains";


+ 6
- 1
bubble-server/src/main/java/bubble/dao/account/AccountDAO.java View File

@@ -42,6 +42,8 @@ import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;


import static bubble.ApiConstants.getRemoteHost; 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.AccountTemplate.copyTemplateObjects;
import static bubble.model.account.AutoUpdatePolicy.EMPTY_AUTO_UPDATE_POLICY; import static bubble.model.account.AutoUpdatePolicy.EMPTY_AUTO_UPDATE_POLICY;
import static bubble.server.BubbleConfiguration.getDEFAULT_LOCALE; 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))); .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) { public Account findById(String id) {
final Account found = findByUuid(id); final Account found = findByUuid(id);


+ 19
- 0
bubble-server/src/main/java/bubble/dao/account/TrustedClientDAO.java View File

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

}

+ 2
- 0
bubble-server/src/main/java/bubble/model/account/Account.java View File

@@ -88,6 +88,7 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci
"name", "termsAgreed", "preferredPlan"); "name", "termsAgreed", "preferredPlan");


public static final String ROOT_USERNAME = "root"; 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 final int EMAIL_MAX_LENGTH = 100;


public static Account sageMask(Account sage) { public static Account sageMask(Account sage) {
@@ -239,6 +240,7 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci


@Transient public String getToken() { return getApiToken(); } @Transient public String getToken() { return getApiToken(); }
public Account setToken(String token) { return setApiToken(token); } public Account setToken(String token) { return setApiToken(token); }
public boolean hasToken () { return !empty(getApiToken()); }


public Account(Account other) { copy(this, other, CREATE_FIELDS); } public Account(Account other) { copy(this, other, CREATE_FIELDS); }




+ 43
- 0
bubble-server/src/main/java/bubble/model/account/TrustedClient.java View File

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

}

+ 56
- 0
bubble-server/src/main/java/bubble/model/account/TrustedClientLoginRequest.java View File

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

}

+ 13
- 0
bubble-server/src/main/java/bubble/model/account/TrustedClientResponse.java View File

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

}

+ 16
- 4
bubble-server/src/main/java/bubble/resources/account/AuthResource.java View File

@@ -104,9 +104,15 @@ public class AuthResource {


public Account updateLastLogin(Account account) { return accountDAO.update(account.setLastLogin()); } 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); if (account.getLastLogin() == null) account.setFirstLogin(true);
return sessionDAO.create(updateLastLogin(account));
return sessionDAO.create(updateLastLogin(account, accountDAO));
} }


@GET @Path(EP_CONFIGS) @GET @Path(EP_CONFIGS)
@@ -352,7 +358,7 @@ public class AuthResource {
// try totp token now // try totp token now
account.setToken(authenticatorService.authenticate(account, policy, new AuthenticatorRequest() account.setToken(authenticatorService.authenticate(account, policy, new AuthenticatorRequest()
.setAccount(account.getUuid()) .setAccount(account.getUuid())
.setAuthenticate(true)
// .setAuthenticate(true)
.setToken(request.getTotpToken()))); .setToken(request.getTotpToken())));
authFactors.removeIf(AccountContact::isAuthenticator); 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}") @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) @POST @Path(EP_VERIFY_KEY)
public Response verifyNodeKey(@Context Request req, public Response verifyNodeKey(@Context Request req,
@Context ContainerRequest ctx, @Context ContainerRequest ctx,
@@ -730,6 +740,8 @@ public class AuthResource {
return ok_empty(); return ok_empty();
} }


@GET @Path(EP_TIME) public Response serverTime() { return ok(now()); }

@Autowired private GeoService geoService; @Autowired private GeoService geoService;


@GET @Path(EP_SUPPORT) @GET @Path(EP_SUPPORT)


+ 124
- 0
bubble-server/src/main/java/bubble/resources/account/TrustedAuthResource.java View File

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

}

+ 6
- 1
bubble-server/src/main/java/bubble/service/account/StandardAuthenticatorService.java View File

@@ -12,6 +12,7 @@ import bubble.model.account.AccountPolicy;
import bubble.model.account.AuthenticatorRequest; import bubble.model.account.AuthenticatorRequest;
import bubble.model.account.message.ActionTarget; import bubble.model.account.message.ActionTarget;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.wizard.cache.redis.RedisService; import org.cobbzilla.wizard.cache.redis.RedisService;
import org.glassfish.jersey.server.ContainerRequest; import org.glassfish.jersey.server.ContainerRequest;
import org.springframework.beans.factory.annotation.Autowired; 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.invalidEx;
import static org.cobbzilla.wizard.resources.ResourceUtil.userPrincipal; import static org.cobbzilla.wizard.resources.ResourceUtil.userPrincipal;


@Service
@Service @Slf4j
public class StandardAuthenticatorService implements AuthenticatorService { public class StandardAuthenticatorService implements AuthenticatorService {


@Autowired private SessionDAO sessionDAO; @Autowired private SessionDAO sessionDAO;
@@ -76,6 +77,10 @@ public class StandardAuthenticatorService implements AuthenticatorService {
if (policy == null || !policy.hasVerifiedAuthenticator()) return; if (policy == null || !policy.hasVerifiedAuthenticator()) return;
if (target != null) { if (target != null) {
final AccountContact authenticator = policy.getAuthenticator(); final AccountContact authenticator = policy.getAuthenticator();
if (authenticator == null) {
log.info("ensureAuthenticated("+account.getName()+"): no authenticator configured");
return;
}
switch (target) { switch (target) {
case account: if (!authenticator.requiredForAccountOperations()) return; break; case account: if (!authenticator.requiredForAccountOperations()) return; break;
case network: if (!authenticator.requiredForNetworkOperations()) return; break; case network: if (!authenticator.requiredForNetworkOperations()) return; break;


+ 5
- 2
bubble-server/src/main/java/bubble/service/cloud/StandardDeviceIdService.java View File

@@ -126,14 +126,17 @@ public class StandardDeviceIdService implements DeviceIdService {
@Override public void setDeviceSecurityLevel(Device device) { @Override public void setDeviceSecurityLevel(Device device) {
if (configuration.testMode()) return; if (configuration.testMode()) return;
for (String ip : findIpsByDevice(device.getUuid())) { 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())) { for (AppSite site : siteDAO.findByAccount(device.getAccount())) {
final String siteKey = REDIS_KEY_DEVICE_SITE_MAX_SECURITY_LEVEL_PREFIX + ip;
if (site.hasMaxSecurityHosts()) { if (site.hasMaxSecurityHosts()) {
final String siteKey = REDIS_KEY_DEVICE_SITE_MAX_SECURITY_LEVEL_PREFIX + ip;
if (site.enableMaxSecurityHosts()) { 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()); redis.sadd_plaintext(siteKey, site.getMaxSecurityHosts());
} else { } 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()); redis.srem(siteKey, site.getMaxSecurityHosts());
} }
} }


+ 2
- 1
bubble-server/src/main/java/bubble/service/dbfilter/FilteredEntityIterator.java View File

@@ -12,6 +12,7 @@ import bubble.dao.device.DeviceDAO;
import bubble.model.account.Account; import bubble.model.account.Account;
import bubble.model.account.HasAccount; import bubble.model.account.HasAccount;
import bubble.model.account.ReferralCode; import bubble.model.account.ReferralCode;
import bubble.model.account.TrustedClient;
import bubble.model.account.message.AccountMessage; import bubble.model.account.message.AccountMessage;
import bubble.model.bill.AccountPayment; import bubble.model.bill.AccountPayment;
import bubble.model.bill.Bill; 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[] { private static final List<Class<? extends Identifiable>> POST_COPY_ENTITIES = Arrays.asList(new Class[] {
BubbleNode.class, BubbleNodeKey.class, Device.class, AccountMessage.class, BubbleNode.class, BubbleNodeKey.class, Device.class, AccountMessage.class,
ReferralCode.class, AccountPayment.class, Bill.class, Promotion.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) { private static boolean isPostCopyEntity(Class<? extends Identifiable> clazz) {


+ 1
- 1
bubble-server/src/main/resources/META-INF/bubble/bubble.properties View File

@@ -1 +1 @@
bubble.version=Adventure 0.15.3
bubble.version=Adventure 0.15.4

+ 14
- 0
bubble-server/src/main/resources/db/migration/V2020072401__add_trusted_client.sql View File

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

+ 1
- 0
bubble-server/src/main/resources/logback.xml View File

@@ -62,6 +62,7 @@
<!-- <logger name="bubble.service.cloud.NodeLauncher" level="DEBUG" />--> <!-- <logger name="bubble.service.cloud.NodeLauncher" level="DEBUG" />-->
<!-- <logger name="bubble.service.cloud.NodeService" level="DEBUG" />--> <!-- <logger name="bubble.service.cloud.NodeService" level="DEBUG" />-->
<!-- <logger name="bubble.service.cloud.NodeProgressMeter" 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.cloud.compute.vultr" level="DEBUG" />-->
<logger name="bubble.resources.message" level="INFO" /> <logger name="bubble.resources.message" level="INFO" />
<logger name="bubble.app.analytics" level="DEBUG" /> <logger name="bubble.app.analytics" level="DEBUG" />


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

@@ -842,6 +842,10 @@ err.suspended.cannotSuspendSelf=You cannot suspend yourself
err.tag.invalid=Tag is invalid err.tag.invalid=Tag is invalid
err.tagsJson.length=Too many tags err.tagsJson.length=Too many tags
err.tagString.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.noRolesDir=No roles directory found in tgz
err.tgzB64.invalid.wrongNumberOfFiles=Wrong number of files in tgz base directory 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 err.tgzB64.invalid.missingTasksMainYml=No tasks/main.yml file found for role in tgz


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

@@ -306,7 +306,7 @@ button_label_forgot_password=Send reset password
forgot_password_login_link=Back to Login forgot_password_login_link=Back to Login


# New UI Labels for Registration # 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_blurb=Block behavior tracking, ads and other rude behaviour.
register_field_label_email=Email address (will be your username) register_field_label_email=Email address (will be your username)
field_label_password=Choose Password field_label_password=Choose Password


+ 10
- 0
bubble-server/src/test/java/bubble/test/live/S3StorageTest.java View File

@@ -7,6 +7,7 @@ package bubble.test.live;
import bubble.cloud.CloudServiceType; import bubble.cloud.CloudServiceType;
import bubble.cloud.storage.s3.S3StorageConfig; import bubble.cloud.storage.s3.S3StorageConfig;
import bubble.dao.cloud.CloudServiceDAO; import bubble.dao.cloud.CloudServiceDAO;
import bubble.model.account.Account;
import bubble.model.cloud.CloudService; import bubble.model.cloud.CloudService;
import bubble.model.cloud.RekeyRequest; import bubble.model.cloud.RekeyRequest;
import bubble.model.cloud.StorageMetadata; 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.io.FileUtil;
import org.cobbzilla.util.security.CryptoUtil; import org.cobbzilla.util.security.CryptoUtil;
import org.cobbzilla.wizard.api.NotFoundException; import org.cobbzilla.wizard.api.NotFoundException;
import org.cobbzilla.wizard.auth.LoginRequest;
import org.junit.Test; import org.junit.Test;


import java.io.*; import java.io.*;
@@ -29,8 +31,11 @@ import java.util.List;
import static bubble.ApiConstants.*; import static bubble.ApiConstants.*;
import static bubble.cloud.storage.StorageCryptStream.MIN_DISTINCT_LENGTH; import static bubble.cloud.storage.StorageCryptStream.MIN_DISTINCT_LENGTH;
import static bubble.cloud.storage.StorageCryptStream.MIN_KEY_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 bubble.model.cloud.CloudCredentials.PARAM_KEY;
import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; 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.HttpContentTypes.APPLICATION_JSON;
import static org.cobbzilla.util.http.HttpMethods.DELETE; import static org.cobbzilla.util.http.HttpMethods.DELETE;
import static org.cobbzilla.util.json.JsonUtil.json; 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); final S3StorageConfig config = json(s3cloud.getDriverConfigJson(), S3StorageConfig.class);
cloudDAO.update(s3cloud.setDriverConfigJson(json(config.setListFetchSize(LIST_FETCH_SIZE)))); 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"; comment = "start with empty storage";
final HttpRequestBean predelete = new HttpRequestBean() final HttpRequestBean predelete = new HttpRequestBean()
.setMethod(DELETE) .setMethod(DELETE)


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

@@ -121,5 +121,164 @@
{"condition": "json.getName() === '{{userAccount.name}}'"} {"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
bubble-web

@@ -1 +1 @@
Subproject commit 74117dccae90c8905c8f4f54d16fcf19ce5eb27e
Subproject commit 55cc17c6c4241b8eb6c1ada2ce29ce08307bbfee

+ 1
- 1
utils/cobbzilla-utils

@@ -1 +1 @@
Subproject commit a39467dbcd062ed1471e44450d495ba7a9bd8942
Subproject commit fc87156268ff3380321b83d9b6e2408441f58047

+ 1
- 1
utils/cobbzilla-wizard

@@ -1 +1 @@
Subproject commit 5510f2d6563ed6109ce153e49acd03ead6a6f4cd
Subproject commit 360ea8067406a3babdf8cd6488a4f5c391ac36bf

Loading…
Cancel
Save