Browse Source

basic add/remove contacts is working

tags/v0.1.6
Jonathan Cobb 5 years ago
parent
commit
a8d8fe8ed2
7 changed files with 90 additions and 33 deletions
  1. +3
    -1
      bubble-server/src/main/java/bubble/cloud/auth/AuthenticatorAuthFieldHandler.java
  2. +4
    -2
      bubble-server/src/main/java/bubble/cloud/auth/EmailAuthFieldHandler.java
  3. +20
    -10
      bubble-server/src/main/java/bubble/model/account/AccountContact.java
  4. +15
    -4
      bubble-server/src/main/java/bubble/model/account/AccountPolicy.java
  5. +17
    -13
      bubble-server/src/main/java/bubble/resources/account/AccountsResource.java
  6. +30
    -2
      bubble-server/src/main/resources/message_templates/server/en_US/post_auth/ResourceMessages.properties
  7. +1
    -1
      bubble-web

+ 3
- 1
bubble-server/src/main/java/bubble/cloud/auth/AuthenticatorAuthFieldHandler.java View File

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


} }

+ 4
- 2
bubble-server/src/main/java/bubble/cloud/auth/EmailAuthFieldHandler.java View File

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


+ 20
- 10
bubble-server/src/main/java/bubble/model/account/AccountContact.java View File

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




+ 15
- 4
bubble-server/src/main/java/bubble/model/account/AccountPolicy.java View File

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

+ 17
- 13
bubble-server/src/main/java/bubble/resources/account/AccountsResource.java View File

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


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

@@ -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
bubble-web

@@ -1 +1 @@
Subproject commit cd6467cb5d641d9aee8c3f3e0f6672b39630ce2a
Subproject commit 180267511f122359e7a00bd9a8903f80d5c1366a

Loading…
Cancel
Save