Selaa lähdekoodia

basic add/remove contacts is working

tags/v0.1.6
Jonathan Cobb 5 vuotta sitten
vanhempi
commit
a8d8fe8ed2
7 muutettua tiedostoa jossa 90 lisäystä ja 33 poistoa
  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 Näytä tiedosto

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

}

+ 4
- 2
bubble-server/src/main/java/bubble/cloud/auth/EmailAuthFieldHandler.java Näytä tiedosto

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


+ 20
- 10
bubble-server/src/main/java/bubble/model/account/AccountContact.java Näytä tiedosto

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



+ 15
- 4
bubble-server/src/main/java/bubble/model/account/AccountPolicy.java Näytä tiedosto

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

+ 17
- 13
bubble-server/src/main/java/bubble/resources/account/AccountsResource.java Näytä tiedosto

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


+ 30
- 2
bubble-server/src/main/resources/message_templates/server/en_US/post_auth/ResourceMessages.properties Näytä tiedosto

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

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

Ladataan…
Peruuta
Tallenna