@@ -7,11 +7,13 @@ import java.util.List; | |||||
public class AuthenticatorAuthFieldHandler implements AuthFieldHandler { | public class AuthenticatorAuthFieldHandler implements AuthFieldHandler { | ||||
public static final String MASKED_INFO = "*".repeat(10); | |||||
@Override public List<ConstraintViolationBean> validate(String val) { | @Override public List<ConstraintViolationBean> validate(String val) { | ||||
// nothing to validate? or should we validate that the val is a proper secret key? | // nothing to validate? or should we validate that the val is a proper secret key? | ||||
return Collections.emptyList(); | 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); | String namePart = val.substring(0, atPos); | ||||
if (namePart.length() > UNMASKED_LEN) { | 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 { | } else { | ||||
namePart = "*".repeat(namePart.length()); | |||||
namePart = namePart.charAt(0)+"*".repeat(namePart.length()); | |||||
} | } | ||||
String hostPart = val.substring(atPos+1); | String hostPart = val.substring(atPos+1); | ||||
@@ -1,11 +1,12 @@ | |||||
package bubble.model.account; | package bubble.model.account; | ||||
import bubble.cloud.CloudServiceType; | import bubble.cloud.CloudServiceType; | ||||
import bubble.model.account.message.AccountAction; | |||||
import bubble.model.account.message.AccountMessage; | import bubble.model.account.message.AccountMessage; | ||||
import bubble.model.account.message.AccountMessageType; | import bubble.model.account.message.AccountMessageType; | ||||
import bubble.model.account.message.AccountAction; | |||||
import bubble.model.account.message.ActionTarget; | import bubble.model.account.message.ActionTarget; | ||||
import com.fasterxml.jackson.annotation.JsonIgnore; | import com.fasterxml.jackson.annotation.JsonIgnore; | ||||
import com.warrenstrange.googleauth.GoogleAuthenticatorKey; | |||||
import lombok.*; | import lombok.*; | ||||
import lombok.experimental.Accessors; | import lombok.experimental.Accessors; | ||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
@@ -15,14 +16,14 @@ import org.cobbzilla.wizard.validation.HasValue; | |||||
import org.cobbzilla.wizard.validation.ValidationResult; | import org.cobbzilla.wizard.validation.ValidationResult; | ||||
import java.io.Serializable; | 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 java.util.function.Predicate; | ||||
import static bubble.ApiConstants.G_AUTH; | import static bubble.ApiConstants.G_AUTH; | ||||
import static java.util.UUID.randomUUID; | import static java.util.UUID.randomUUID; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | 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.util.reflect.ReflectionUtil.copy; | ||||
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; | 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 uuid = randomUUID().toString(); | ||||
@Getter @Setter private String nick; | @Getter @Setter private String nick; | ||||
public AccountContact(AccountContact other) { copy(this, other); } | |||||
public boolean hasNick () { return !empty(nick); } | public boolean hasNick () { return !empty(nick); } | ||||
public boolean sameNick (String n) { return !empty(nick) && !empty(n) && nick.equals(n); } | 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); | if (errors != null && !errors.isEmpty()) throw invalidEx(errors); | ||||
// there must be at least one contact that can be used to unlock the network | // 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()) { | if (c.isAuthenticator()) { | ||||
@@ -113,13 +117,21 @@ public class AccountContact implements Serializable { | |||||
if (c.hasNick()) checkNickInUse(c, contacts); | if (c.hasNick()) checkNickInUse(c, contacts); | ||||
// generate secret key if needed | // 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 ArrayUtil.append(contacts, c); | ||||
} | } | ||||
return contacts; | 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) { | private static void checkNickInUse(AccountContact c, AccountContact[] contacts) { | ||||
if (Arrays.stream(contacts).anyMatch(cc -> cc.sameNick(c.getNick()))) { | if (Arrays.stream(contacts).anyMatch(cc -> cc.sameNick(c.getNick()))) { | ||||
// there is another contact with the new nick, cannot set it | // there is another contact with the new nick, cannot set it | ||||
@@ -252,9 +264,7 @@ public class AccountContact implements Serializable { | |||||
} | } | ||||
public AccountContact mask() { | public AccountContact mask() { | ||||
return new AccountContact() | |||||
.setNick(getNick()) | |||||
.setType(getType()) | |||||
return new AccountContact(this) | |||||
.setInfo(getType().mask(getInfo())); | .setInfo(getType().mask(getInfo())); | ||||
} | } | ||||
@@ -18,6 +18,7 @@ import org.hibernate.annotations.Type; | |||||
import javax.persistence.*; | import javax.persistence.*; | ||||
import javax.validation.constraints.Size; | import javax.validation.constraints.Size; | ||||
import java.util.ArrayList; | |||||
import java.util.Arrays; | import java.util.Arrays; | ||||
import java.util.Collections; | import java.util.Collections; | ||||
import java.util.List; | 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.daemon.ZillaRuntime.empty; | ||||
import static org.cobbzilla.util.json.JsonUtil.json; | import static org.cobbzilla.util.json.JsonUtil.json; | ||||
import static org.cobbzilla.util.reflect.ReflectionUtil.copy; | 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 | @Entity @ECType | ||||
@NoArgsConstructor @Accessors(chain=true) | @NoArgsConstructor @Accessors(chain=true) | ||||
@@ -54,10 +54,10 @@ public class AccountPolicy extends IdentifiableBase implements HasAccount { | |||||
@JsonIgnore @Override public String getName() { return getAccount(); } | @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); | @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); | @Getter @Setter private Long accountOperationTimeout = MINUTES.toMillis(10); | ||||
@Enumerated(EnumType.STRING) @Column(length=20, nullable=false) | @Enumerated(EnumType.STRING) @Column(length=20, nullable=false) | ||||
@@ -221,4 +221,15 @@ public class AccountPolicy extends IdentifiableBase implements HasAccount { | |||||
} | } | ||||
return result; | 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, | public Response viewPolicy(@Context ContainerRequest ctx, | ||||
@PathParam("id") String id) { | @PathParam("id") String id) { | ||||
final AccountContext c = new AccountContext(ctx, 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) | @POST @Path("/{id}"+EP_POLICY) | ||||
@@ -134,7 +135,7 @@ public class AccountsResource { | |||||
} else { | } else { | ||||
policy = policyDAO.update((AccountPolicy) policy.update(request)); | policy = policyDAO.update((AccountPolicy) policy.update(request)); | ||||
} | } | ||||
return ok(policy); | |||||
return ok(policy.mask()); | |||||
} | } | ||||
@POST @Path("/{id}"+EP_POLICY+EP_CONTACTS) | @POST @Path("/{id}"+EP_POLICY+EP_CONTACTS) | ||||
@@ -144,7 +145,11 @@ public class AccountsResource { | |||||
@Valid AccountContact contact) { | @Valid AccountContact contact) { | ||||
final AccountContext c = new AccountContext(ctx, id); | final AccountContext c = new AccountContext(ctx, id); | ||||
final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); | 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); | final AccountContact added = policy.findContact(contact); | ||||
if (added == null) { | if (added == null) { | ||||
log.error("setContact: contact not set: "+contact); | log.error("setContact: contact not set: "+contact); | ||||
@@ -154,7 +159,7 @@ public class AccountsResource { | |||||
log.info("setContact: contact is new, sending verify message"); | log.info("setContact: contact is new, sending verify message"); | ||||
messageDAO.sendVerifyRequest(getRemoteHost(req), c.account, contact); | messageDAO.sendVerifyRequest(getRemoteHost(req), c.account, contact); | ||||
} | } | ||||
return ok(updated); | |||||
return ok(added); | |||||
} | } | ||||
@POST @Path("/{id}"+EP_POLICY+EP_CONTACTS+"/verify") | @POST @Path("/{id}"+EP_POLICY+EP_CONTACTS+"/verify") | ||||
@@ -182,7 +187,7 @@ public class AccountsResource { | |||||
final AccountContext c = new AccountContext(ctx, id); | final AccountContext c = new AccountContext(ctx, id); | ||||
final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); | final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); | ||||
final AccountContact contact = policy.findContact(new AccountContact().setType(type).setInfo(info)); | 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}") | @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 AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); | ||||
final AccountContact contact = policy.findContact(new AccountContact().setType(type).setInfo(info)); | final AccountContact contact = policy.findContact(new AccountContact().setType(type).setInfo(info)); | ||||
if (contact == null) return notFound(type.name()+"/"+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("id") String id, | ||||
@PathParam("type") CloudServiceType type) { | |||||
@PathParam("uuid") String uuid) { | |||||
final AccountContext c = new AccountContext(ctx, id); | 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 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) | @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_info=Contact Info | ||||
field_label_policy_contact_nick=Nickname | field_label_policy_contact_nick=Nickname | ||||
field_label_policy_contact_type=Type | 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=Email | ||||
field_label_policy_contact_type_email_field=Email Address | |||||
field_label_policy_contact_type_sms=SMS | 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_type_authenticator=Authentication App | ||||
field_label_policy_contact_verified=Verified | field_label_policy_contact_verified=Verified | ||||
field_label_policy_contact_requiredForNetworkUnlock=Required to unlock a new Bubble | 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=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=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=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=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=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=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=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_authFactors=not_required,required,sufficient | ||||
field_label_policy_contact_authFactor=Authentication Factor | 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=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_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=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_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=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_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 | # Networks table | ||||
loading_networks=Loading bubbles... | loading_networks=Loading bubbles... | ||||
@@ -182,7 +210,6 @@ err.cloud.required=Cloud is required | |||||
err.cloudServiceType.required=Cloud type is required | err.cloudServiceType.required=Cloud type is required | ||||
err.cloudType.invalid=Cloud type is invalid | err.cloudType.invalid=Cloud type is invalid | ||||
err.configJson.length=Configuration JSON is too long | 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.contact.required=Contact information is required | ||||
err.contactType.required=Contact type is required | err.contactType.required=Contact type is required | ||||
err.contact.unverified=Cannot set auth factor on an unverified contact; verify first | 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.length=Remote host is too long | ||||
err.remoteHost.required=Remote host is required | err.remoteHost.required=Remote host is required | ||||
err.request.invalid=Request is invalid | 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.invalid=Restore key is invalid | ||||
err.restoreKey.required=Restore key is required | err.restoreKey.required=Restore key is required | ||||
err.role.exists=Role already exists with this name | err.role.exists=Role already exists with this name | ||||
@@ -1 +1 @@ | |||||
Subproject commit cd6467cb5d641d9aee8c3f3e0f6672b39630ce2a | |||||
Subproject commit 180267511f122359e7a00bd9a8903f80d5c1366a |