From a8d8fe8ed2e201ece915ceeb981760445279c782 Mon Sep 17 00:00:00 2001 From: Jonathan Cobb Date: Wed, 18 Dec 2019 19:46:50 -0500 Subject: [PATCH] basic add/remove contacts is working --- .../auth/AuthenticatorAuthFieldHandler.java | 4 ++- .../cloud/auth/EmailAuthFieldHandler.java | 6 ++-- .../bubble/model/account/AccountContact.java | 30 +++++++++++------ .../bubble/model/account/AccountPolicy.java | 19 ++++++++--- .../resources/account/AccountsResource.java | 30 +++++++++-------- .../post_auth/ResourceMessages.properties | 32 +++++++++++++++++-- bubble-web | 2 +- 7 files changed, 90 insertions(+), 33 deletions(-) diff --git a/bubble-server/src/main/java/bubble/cloud/auth/AuthenticatorAuthFieldHandler.java b/bubble-server/src/main/java/bubble/cloud/auth/AuthenticatorAuthFieldHandler.java index f99898a9..08177f81 100644 --- a/bubble-server/src/main/java/bubble/cloud/auth/AuthenticatorAuthFieldHandler.java +++ b/bubble-server/src/main/java/bubble/cloud/auth/AuthenticatorAuthFieldHandler.java @@ -7,11 +7,13 @@ import java.util.List; public class AuthenticatorAuthFieldHandler implements AuthFieldHandler { + public static final String MASKED_INFO = "*".repeat(10); + @Override public List 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; } } diff --git a/bubble-server/src/main/java/bubble/cloud/auth/EmailAuthFieldHandler.java b/bubble-server/src/main/java/bubble/cloud/auth/EmailAuthFieldHandler.java index 662945cb..25f5722d 100644 --- a/bubble-server/src/main/java/bubble/cloud/auth/EmailAuthFieldHandler.java +++ b/bubble-server/src/main/java/bubble/cloud/auth/EmailAuthFieldHandler.java @@ -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); diff --git a/bubble-server/src/main/java/bubble/model/account/AccountContact.java b/bubble-server/src/main/java/bubble/model/account/AccountContact.java index 96fa8841..d42c9529 100644 --- a/bubble-server/src/main/java/bubble/model/account/AccountContact.java +++ b/bubble-server/src/main/java/bubble/model/account/AccountContact.java @@ -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 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())); } diff --git a/bubble-server/src/main/java/bubble/model/account/AccountPolicy.java b/bubble-server/src/main/java/bubble/model/account/AccountPolicy.java index c2717300..0b76c4d0 100644 --- a/bubble-server/src/main/java/bubble/model/account/AccountPolicy.java +++ b/bubble-server/src/main/java/bubble/model/account/AccountPolicy.java @@ -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 scrubbed = new ArrayList<>(); + for (AccountContact c : getAccountContacts()) { + scrubbed.add(c.mask()); + } + setAccountContacts(scrubbed.toArray(new AccountContact[0])); + } + return this; + } } diff --git a/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java b/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java index 161a273b..75ff4caf 100644 --- a/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java @@ -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) diff --git a/bubble-server/src/main/resources/message_templates/server/en_US/post_auth/ResourceMessages.properties b/bubble-server/src/main/resources/message_templates/server/en_US/post_auth/ResourceMessages.properties index 839203cc..f7d3b004 100644 --- a/bubble-server/src/main/resources/message_templates/server/en_US/post_auth/ResourceMessages.properties +++ b/bubble-server/src/main/resources/message_templates/server/en_US/post_auth/ResourceMessages.properties @@ -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 diff --git a/bubble-web b/bubble-web index cd6467cb..18026751 160000 --- a/bubble-web +++ b/bubble-web @@ -1 +1 @@ -Subproject commit cd6467cb5d641d9aee8c3f3e0f6672b39630ce2a +Subproject commit 180267511f122359e7a00bd9a8903f80d5c1366a