@@ -7,11 +7,13 @@ import java.util.List; | |||
public class AuthenticatorAuthFieldHandler implements AuthFieldHandler { | |||
public static final String MASKED_INFO = "*".repeat(10); | |||
@Override public List<ConstraintViolationBean> validate(String val) { | |||
// nothing to validate? or should we validate that the val is a proper secret key? | |||
return Collections.emptyList(); | |||
} | |||
@Override public String mask(String val) { return "*".repeat(10); } | |||
@Override public String mask(String val) { return MASKED_INFO; } | |||
} |
@@ -28,9 +28,11 @@ public class EmailAuthFieldHandler implements AuthFieldHandler { | |||
String namePart = val.substring(0, atPos); | |||
if (namePart.length() > UNMASKED_LEN) { | |||
namePart = namePart.substring(0, UNMASKED_LEN) + "*".repeat(namePart.length()- UNMASKED_LEN); | |||
namePart = namePart.substring(0, UNMASKED_LEN) + "*".repeat(namePart.length() - UNMASKED_LEN); | |||
} else if (namePart.length() == 1) { | |||
namePart = "**"; | |||
} else { | |||
namePart = "*".repeat(namePart.length()); | |||
namePart = namePart.charAt(0)+"*".repeat(namePart.length()); | |||
} | |||
String hostPart = val.substring(atPos+1); | |||
@@ -1,11 +1,12 @@ | |||
package bubble.model.account; | |||
import bubble.cloud.CloudServiceType; | |||
import bubble.model.account.message.AccountAction; | |||
import bubble.model.account.message.AccountMessage; | |||
import bubble.model.account.message.AccountMessageType; | |||
import bubble.model.account.message.AccountAction; | |||
import bubble.model.account.message.ActionTarget; | |||
import com.fasterxml.jackson.annotation.JsonIgnore; | |||
import com.warrenstrange.googleauth.GoogleAuthenticatorKey; | |||
import lombok.*; | |||
import lombok.experimental.Accessors; | |||
import lombok.extern.slf4j.Slf4j; | |||
@@ -15,14 +16,14 @@ import org.cobbzilla.wizard.validation.HasValue; | |||
import org.cobbzilla.wizard.validation.ValidationResult; | |||
import java.io.Serializable; | |||
import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.List; | |||
import java.util.*; | |||
import java.util.function.Predicate; | |||
import static bubble.ApiConstants.G_AUTH; | |||
import static java.util.UUID.randomUUID; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | |||
import static org.cobbzilla.util.json.JsonUtil.COMPACT_MAPPER; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
import static org.cobbzilla.util.reflect.ReflectionUtil.copy; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; | |||
@@ -31,6 +32,9 @@ public class AccountContact implements Serializable { | |||
@Getter @Setter private String uuid = randomUUID().toString(); | |||
@Getter @Setter private String nick; | |||
public AccountContact(AccountContact other) { copy(this, other); } | |||
public boolean hasNick () { return !empty(nick); } | |||
public boolean sameNick (String n) { return !empty(nick) && !empty(n) && nick.equals(n); } | |||
@@ -71,8 +75,8 @@ public class AccountContact implements Serializable { | |||
if (errors != null && !errors.isEmpty()) throw invalidEx(errors); | |||
// there must be at least one contact that can be used to unlock the network | |||
if (!c.requiredForNetworkUnlock() && Arrays.stream(contacts).noneMatch(AccountContact::requiredForNetworkUnlock)) { | |||
throw invalidEx("err.contact.atLeastOneNetworkUnlockContactRequired"); | |||
if (!c.requiredForNetworkUnlock() && (contacts == null || Arrays.stream(contacts).noneMatch(AccountContact::requiredForNetworkUnlock))) { | |||
throw invalidEx("err.requiredForNetworkUnlock.atLeastOneNetworkUnlockContactRequired"); | |||
} | |||
if (c.isAuthenticator()) { | |||
@@ -113,13 +117,21 @@ public class AccountContact implements Serializable { | |||
if (c.hasNick()) checkNickInUse(c, contacts); | |||
// generate secret key if needed | |||
if (c.isAuthenticator()) c.setInfo(G_AUTH.createCredentials().getKey()); | |||
if (c.isAuthenticator()) c.setInfo(getTotpInfo()); | |||
return ArrayUtil.append(contacts, c); | |||
} | |||
return contacts; | |||
} | |||
public static String getTotpInfo() { | |||
final Map<String, Object> totpMap = new HashMap<>(); | |||
final GoogleAuthenticatorKey creds = G_AUTH.createCredentials(); | |||
totpMap.put("key", creds.getKey()); | |||
totpMap.put("backupCodes", creds.getScratchCodes()); | |||
return json(totpMap, COMPACT_MAPPER); | |||
} | |||
private static void checkNickInUse(AccountContact c, AccountContact[] contacts) { | |||
if (Arrays.stream(contacts).anyMatch(cc -> cc.sameNick(c.getNick()))) { | |||
// there is another contact with the new nick, cannot set it | |||
@@ -252,9 +264,7 @@ public class AccountContact implements Serializable { | |||
} | |||
public AccountContact mask() { | |||
return new AccountContact() | |||
.setNick(getNick()) | |||
.setType(getType()) | |||
return new AccountContact(this) | |||
.setInfo(getType().mask(getInfo())); | |||
} | |||
@@ -18,6 +18,7 @@ import org.hibernate.annotations.Type; | |||
import javax.persistence.*; | |||
import javax.validation.constraints.Size; | |||
import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.Collections; | |||
import java.util.List; | |||
@@ -30,8 +31,7 @@ import static org.cobbzilla.util.daemon.ZillaRuntime.die; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
import static org.cobbzilla.util.reflect.ReflectionUtil.copy; | |||
import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENCRYPTED_STRING; | |||
import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENC_PAD; | |||
import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.*; | |||
@Entity @ECType | |||
@NoArgsConstructor @Accessors(chain=true) | |||
@@ -54,10 +54,10 @@ public class AccountPolicy extends IdentifiableBase implements HasAccount { | |||
@JsonIgnore @Override public String getName() { return getAccount(); } | |||
@Column(nullable=false) | |||
@Type(type=ENCRYPTED_LONG) @Column(columnDefinition="varchar("+ENC_LONG+") NOT NULL") | |||
@Getter @Setter private Long nodeOperationTimeout = MINUTES.toMillis(30); | |||
@Column(nullable=false) | |||
@Type(type=ENCRYPTED_LONG) @Column(columnDefinition="varchar("+ENC_LONG+") NOT NULL") | |||
@Getter @Setter private Long accountOperationTimeout = MINUTES.toMillis(10); | |||
@Enumerated(EnumType.STRING) @Column(length=20, nullable=false) | |||
@@ -221,4 +221,15 @@ public class AccountPolicy extends IdentifiableBase implements HasAccount { | |||
} | |||
return result; | |||
} | |||
public AccountPolicy mask() { | |||
if (hasAccountContacts()) { | |||
final List<AccountContact> scrubbed = new ArrayList<>(); | |||
for (AccountContact c : getAccountContacts()) { | |||
scrubbed.add(c.mask()); | |||
} | |||
setAccountContacts(scrubbed.toArray(new AccountContact[0])); | |||
} | |||
return this; | |||
} | |||
} |
@@ -120,7 +120,8 @@ public class AccountsResource { | |||
public Response viewPolicy(@Context ContainerRequest ctx, | |||
@PathParam("id") String id) { | |||
final AccountContext c = new AccountContext(ctx, id); | |||
return ok(policyDAO.findSingleByAccount(c.account.getUuid())); | |||
final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); | |||
return policy == null ? notFound(id) : ok(policy.mask()); | |||
} | |||
@POST @Path("/{id}"+EP_POLICY) | |||
@@ -134,7 +135,7 @@ public class AccountsResource { | |||
} else { | |||
policy = policyDAO.update((AccountPolicy) policy.update(request)); | |||
} | |||
return ok(policy); | |||
return ok(policy.mask()); | |||
} | |||
@POST @Path("/{id}"+EP_POLICY+EP_CONTACTS) | |||
@@ -144,7 +145,11 @@ public class AccountsResource { | |||
@Valid AccountContact contact) { | |||
final AccountContext c = new AccountContext(ctx, id); | |||
final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); | |||
final AccountPolicy updated = policyDAO.update(policy.setContact(contact)); | |||
final AccountContact existing = policy.findContact(contact); | |||
if (existing != null && existing.isAuthenticator()) return invalid("err.authenticator.configured"); | |||
policyDAO.update(policy.setContact(contact)); | |||
final AccountContact added = policy.findContact(contact); | |||
if (added == null) { | |||
log.error("setContact: contact not set: "+contact); | |||
@@ -154,7 +159,7 @@ public class AccountsResource { | |||
log.info("setContact: contact is new, sending verify message"); | |||
messageDAO.sendVerifyRequest(getRemoteHost(req), c.account, contact); | |||
} | |||
return ok(updated); | |||
return ok(added); | |||
} | |||
@POST @Path("/{id}"+EP_POLICY+EP_CONTACTS+"/verify") | |||
@@ -182,7 +187,7 @@ public class AccountsResource { | |||
final AccountContext c = new AccountContext(ctx, id); | |||
final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); | |||
final AccountContact contact = policy.findContact(new AccountContact().setType(type).setInfo(info)); | |||
return contact != null ? ok(contact) : notFound(type+"/"+info); | |||
return contact != null ? ok(contact.mask()) : notFound(type+"/"+info); | |||
} | |||
@DELETE @Path("/{id}"+EP_POLICY+EP_CONTACTS+"/{type}/{info}") | |||
@@ -195,19 +200,18 @@ public class AccountsResource { | |||
final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); | |||
final AccountContact contact = policy.findContact(new AccountContact().setType(type).setInfo(info)); | |||
if (contact == null) return notFound(type.name()+"/"+info); | |||
return ok(policyDAO.update(policy.removeContact(contact))); | |||
return ok(policyDAO.update(policy.removeContact(contact)).mask()); | |||
} | |||
@DELETE @Path("/{id}"+EP_POLICY+EP_CONTACTS+"/{type}") | |||
public Response removeAuthenticator(@Context ContainerRequest ctx, | |||
@DELETE @Path("/{id}"+EP_POLICY+EP_CONTACTS+"/{uuid}") | |||
public Response removeContactByUuid(@Context ContainerRequest ctx, | |||
@PathParam("id") String id, | |||
@PathParam("type") CloudServiceType type) { | |||
@PathParam("uuid") String uuid) { | |||
final AccountContext c = new AccountContext(ctx, id); | |||
if (type != CloudServiceType.authenticator) return invalid("err.info.required", "info is required"); | |||
final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); | |||
final AccountContact authenticator = policy.getAuthenticator(); | |||
if (authenticator == null) return notFound(CloudServiceType.authenticator.name()); | |||
return ok(policyDAO.update(policy.removeContact(authenticator))); | |||
final AccountContact found = policy.findContact(new AccountContact().setUuid(uuid)); | |||
if (found == null) return notFound(uuid); | |||
return ok(policyDAO.update(policy.removeContact(found)).mask()); | |||
} | |||
@DELETE @Path("/{id}"+EP_REQUEST) | |||
@@ -66,27 +66,55 @@ form_label_title_account_contacts=Contacts and Authorization | |||
field_label_policy_contact_info=Contact Info | |||
field_label_policy_contact_nick=Nickname | |||
field_label_policy_contact_type=Type | |||
field_label_policy_contact_types=email,sms,authenticator | |||
field_label_policy_contact_type_options=email,sms,authenticator | |||
field_label_policy_contact_type_email=Email | |||
field_label_policy_contact_type_email_field=Email Address | |||
field_label_policy_contact_type_sms=SMS | |||
field_label_policy_contact_type_sms_field=SMS-Enabled Phone Number | |||
field_label_policy_contact_type_authenticator=Authentication App | |||
field_label_policy_contact_verified=Verified | |||
field_label_policy_contact_requiredForNetworkUnlock=Required to unlock a new Bubble | |||
field_label_policy_contact_requiredForNetworkUnlock_icon=fa fa-unlock | |||
field_label_policy_contact_requiredForNodeOperations=Required for operations on your Bubble | |||
field_label_policy_contact_requiredForNodeOperations_icon=fa fa-cloud | |||
field_label_policy_contact_requiredForAccountOperations=Required for operations on your Account | |||
field_label_policy_contact_requiredForAccountOperations_icon=fa fa-user | |||
field_label_policy_contact_receiveVerifyNotifications=Required for verification of newly added Contacts/Authorizations | |||
field_label_policy_contact_receiveVerifyNotifications_icon=fa fa-question-circle | |||
field_label_policy_contact_receiveLoginNotifications=Receive login notifications | |||
field_label_policy_contact_receiveLoginNotifications_icon=fa fa-sign-in-alt | |||
field_label_policy_contact_receivePasswordNotification=Receive change password notifications | |||
field_label_policy_contact_receivePasswordNotification_icon=fa fa-key | |||
field_label_policy_contact_receiveInformationalMessages=Receive informational messages | |||
field_label_policy_contact_receiveInformationalMessages_icon=fa fa-info | |||
field_label_policy_contact_receivePromotionalMessages=Receive promotional messages | |||
field_label_policy_contact_receivePromotionalMessages_icon=fa fa-bullhorn | |||
field_label_policy_contact_authFactors=not_required,required,sufficient | |||
field_label_policy_contact_authFactor=Authentication Factor | |||
field_label_policy_contact_authFactor_icon=fa fa-passport | |||
field_label_policy_contact_authFactor_name_not_required=Not Required | |||
field_label_policy_contact_authFactor_name_not_required_icon=fa fa-circle | |||
field_label_policy_contact_authFactor_description_not_required=Not a required Auth Factor to approve any operation | |||
field_label_policy_contact_authFactor_name_required=Required | |||
field_label_policy_contact_authFactor_name_required_icon=fa fa-check-double | |||
field_label_policy_contact_authFactor_description_required=Always a required Auth Factor to approve any operation | |||
field_label_policy_contact_authFactor_name_sufficient=Sufficient | |||
field_label_policy_contact_authFactor_name_sufficient_icon=fa fa-check | |||
field_label_policy_contact_authFactor_description_sufficient=If an operation is approved via this method (in addition to approval by all Required Auth Factors, if any), then the operation will be allowed | |||
field_label_policy_contact_value_enabled_icon=fa fa-check | |||
field_label_policy_contact_value_enabled_name=Enabled | |||
field_label_policy_contact_value_disabled_icon=fa fa-circle | |||
field_label_policy_contact_value_disabled_name=Disabled | |||
field_label_policy_contact_value_not_applicable_icon=fa fa-times-circle | |||
field_label_policy_contact_value_not_applicable_name=N/A | |||
button_label_edit_contact=Edit | |||
button_label_edit_contact_icon=fa fa-edit | |||
button_label_remove_contact=Remove | |||
button_label_remove_contact_icon=fa fa-trash-alt | |||
form_label_title_account_add_contact=Add New Contact/Authorization | |||
button_label_add_contact=Add | |||
# Networks table | |||
loading_networks=Loading bubbles... | |||
@@ -182,7 +210,6 @@ err.cloud.required=Cloud is required | |||
err.cloudServiceType.required=Cloud type is required | |||
err.cloudType.invalid=Cloud type is invalid | |||
err.configJson.length=Configuration JSON is too long | |||
err.contact.atLeastOneNetworkUnlockContactRequired=You must have at least one verified contact method that can unlock your network | |||
err.contact.required=Contact information is required | |||
err.contactType.required=Contact type is required | |||
err.contact.unverified=Cannot set auth factor on an unverified contact; verify first | |||
@@ -310,6 +337,7 @@ err.refund.unknownError=An error occurred processing your refund. Please contact | |||
err.remoteHost.length=Remote host is too long | |||
err.remoteHost.required=Remote host is required | |||
err.request.invalid=Request is invalid | |||
err.requiredForNetworkUnlock.atLeastOneNetworkUnlockContactRequired=You must have at least one verified contact method that can unlock a new Bubble | |||
err.restoreKey.invalid=Restore key is invalid | |||
err.restoreKey.required=Restore key is required | |||
err.role.exists=Role already exists with this name | |||
@@ -1 +1 @@ | |||
Subproject commit cd6467cb5d641d9aee8c3f3e0f6672b39630ce2a | |||
Subproject commit 180267511f122359e7a00bd9a8903f80d5c1366a |