@@ -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"; | ||||
@@ -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); | ||||
@@ -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"); | "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); } | ||||
@@ -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 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) | ||||
@@ -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.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; | ||||
@@ -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()); | ||||
} | } | ||||
} | } | ||||
@@ -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.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.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" /> | ||||
@@ -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 | ||||
@@ -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 | ||||
@@ -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) | ||||
@@ -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 @@ | |||||
Subproject commit 74117dccae90c8905c8f4f54d16fcf19ce5eb27e | |||||
Subproject commit 55cc17c6c4241b8eb6c1ada2ce29ce08307bbfee |
@@ -1 +1 @@ | |||||
Subproject commit a39467dbcd062ed1471e44450d495ba7a9bd8942 | |||||
Subproject commit fc87156268ff3380321b83d9b6e2408441f58047 |
@@ -1 +1 @@ | |||||
Subproject commit 5510f2d6563ed6109ce153e49acd03ead6a6f4cd | |||||
Subproject commit 360ea8067406a3babdf8cd6488a4f5c391ac36bf |