@@ -237,7 +237,20 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci | |||
public boolean sendWelcomeEmail() { return sendWelcomeEmail != null && sendWelcomeEmail; } | |||
@Transient @Getter @Setter private transient String loginRequest; | |||
@Transient @Getter @Setter private transient AccountContact[] multifactorAuth; | |||
@Transient @Getter private transient AccountContact[] multifactorAuth; | |||
public Account setMultifactorAuth(AccountContact[] mfa) { | |||
if (!empty(mfa)) { | |||
final AccountContact[] masked = new AccountContact[mfa.length]; | |||
for (int i=0; i<mfa.length; i++) masked[i] = mfa[i].mask(); | |||
this.multifactorAuth = masked; | |||
} else { | |||
this.multifactorAuth = null; | |||
} | |||
return this; | |||
} | |||
public Account setMultifactorAuthList (List<AccountContact> mfa) { | |||
return setMultifactorAuth(empty(mfa) ? null : mfa.stream().map(AccountContact::mask).toArray(AccountContact[]::new)); | |||
} | |||
@Transient @Getter @Setter private transient String remoteHost; | |||
@Transient @JsonIgnore @Getter @Setter private transient Boolean verifyContact; | |||
@@ -141,6 +141,13 @@ public class AccountPolicy extends IdentifiableBase implements HasAccount { | |||
} | |||
} | |||
public List<AccountContact> getRequiredExternalApprovals(AccountMessage message) { | |||
final List<AccountContact> required = getRequiredApprovals(message); | |||
return required.isEmpty() ? required : required.stream() | |||
.filter(AccountContact::isNotAuthenticator) | |||
.collect(Collectors.toList()); | |||
} | |||
public List<AccountContact> requiredAuthFactors() { | |||
return Arrays.stream(getAccountContacts()) | |||
.filter(AccountContact::requiredAuthFactor) | |||
@@ -1,6 +1,7 @@ | |||
package bubble.resources.account; | |||
import bubble.cloud.CloudServiceType; | |||
import bubble.dao.SessionDAO; | |||
import bubble.dao.account.AccountDAO; | |||
import bubble.dao.account.AccountPolicyDAO; | |||
import bubble.dao.account.message.AccountMessageDAO; | |||
@@ -26,6 +27,8 @@ import bubble.service.account.download.AccountDownloadService; | |||
import bubble.service.boot.SelfNodeService; | |||
import bubble.service.cloud.StandardNetworkService; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.wizard.auth.ChangePasswordRequest; | |||
import org.cobbzilla.wizard.model.HashedPassword; | |||
import org.cobbzilla.wizard.validation.ConstraintViolationBean; | |||
import org.cobbzilla.wizard.validation.ValidationResult; | |||
import org.glassfish.grizzly.http.server.Request; | |||
@@ -43,6 +46,7 @@ import java.util.Map; | |||
import static bubble.ApiConstants.*; | |||
import static bubble.model.account.Account.ADMIN_UPDATE_FIELDS; | |||
import static bubble.model.account.Account.validatePassword; | |||
import static bubble.resources.account.AuthResource.forgotPasswordMessage; | |||
import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||
@@ -60,6 +64,7 @@ public class AccountsResource { | |||
@Autowired private AccountDownloadService downloadService; | |||
@Autowired private AuthenticatorService authenticatorService; | |||
@Autowired private SelfNodeService selfNodeService; | |||
@Autowired private SessionDAO sessionDAO; | |||
@GET | |||
public Response list(@Context ContainerRequest ctx) { | |||
@@ -310,6 +315,54 @@ public class AccountsResource { | |||
.setRemoteHost(getRemoteHost(req)))); | |||
} | |||
@POST @Path("/{id}"+EP_CHANGE_PASSWORD) | |||
public Response rootChangePassword(@Context Request req, | |||
@Context ContainerRequest ctx, | |||
@PathParam("id") String id, | |||
ChangePasswordRequest request) { | |||
final AccountContext c = new AccountContext(ctx, id); | |||
if (!c.caller.admin()) return forbidden(); | |||
final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); | |||
if (policy != null && request.hasTotpToken()) { | |||
authenticatorService.authenticate(c.account, policy, new AuthenticatorRequest() | |||
.setAccount(c.account.getUuid()) | |||
.setAuthenticate(true) | |||
.setToken(request.getTotpToken())); | |||
} | |||
if (c.caller.getUuid().equals(c.account.getUuid()) || c.account.admin()) { | |||
if (policy != null) authenticatorService.ensureAuthenticated(ctx, policy, ActionTarget.account); | |||
if (!c.account.getHashedPassword().isCorrectPassword(request.getOldPassword())) { | |||
return invalid("err.currentPassword.invalid", "current password was invalid", ""); | |||
} | |||
} | |||
final ConstraintViolationBean passwordViolation = validatePassword(request.getNewPassword()); | |||
if (passwordViolation != null) return invalid(passwordViolation); | |||
if (policy != null) { | |||
final AccountMessage forgotPasswordMessage = forgotPasswordMessage(req, c.account, configuration); | |||
final List<AccountContact> requiredApprovals = policy.getRequiredExternalApprovals(forgotPasswordMessage); | |||
if (!requiredApprovals.isEmpty()) { | |||
messageDAO.create(forgotPasswordMessage); | |||
return ok(c.account.setMultifactorAuthList(requiredApprovals)); | |||
} | |||
} | |||
c.account.setHashedPassword(new HashedPassword(request.getNewPassword())); | |||
// Update account | |||
final Account updated = accountDAO.update(c.account); | |||
if (c.caller.getUuid().equals(c.account.getUuid())) { | |||
sessionDAO.update(c.caller.getApiToken(), updated); | |||
} else { | |||
sessionDAO.invalidateAllSessions(c.account.getUuid()); | |||
} | |||
return ok(updated); | |||
} | |||
@DELETE @Path("/{id}") | |||
public Response rootDeleteUser(@Context ContainerRequest ctx, | |||
@PathParam("id") String id) { | |||
@@ -262,15 +262,19 @@ public class AuthResource { | |||
final Account account = accountDAO.findById(request.getName()); | |||
if (account == null) return ok(); | |||
accountMessageDAO.create(new AccountMessage() | |||
accountMessageDAO.create(forgotPasswordMessage(req, account, configuration)); | |||
return ok(); | |||
} | |||
public static AccountMessage forgotPasswordMessage(Request req, Account account, BubbleConfiguration configuration) { | |||
return new AccountMessage() | |||
.setAccount(account.getUuid()) | |||
.setNetwork(configuration.getThisNetwork().getUuid()) | |||
.setName(account.getUuid()) | |||
.setMessageType(AccountMessageType.request) | |||
.setAction(AccountAction.password) | |||
.setTarget(ActionTarget.account) | |||
.setRemoteHost(getRemoteHost(req))); | |||
return ok(); | |||
.setRemoteHost(getRemoteHost(req)); | |||
} | |||
@POST @Path(EP_APPROVE+"/{token}") | |||
@@ -3,8 +3,11 @@ package bubble.resources.account; | |||
import bubble.dao.SessionDAO; | |||
import bubble.dao.account.AccountDAO; | |||
import bubble.dao.account.AccountPolicyDAO; | |||
import bubble.dao.account.message.AccountMessageDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.account.AccountContact; | |||
import bubble.model.account.AccountPolicy; | |||
import bubble.model.account.AuthenticatorRequest; | |||
import bubble.model.account.message.AccountMessage; | |||
import bubble.model.account.message.AccountMessageType; | |||
import bubble.model.account.message.ActionTarget; | |||
@@ -36,6 +39,7 @@ import org.cobbzilla.wizard.client.script.ApiRunnerListener; | |||
import org.cobbzilla.wizard.client.script.ApiRunnerListenerStreamLogger; | |||
import org.cobbzilla.wizard.client.script.ApiScript; | |||
import org.cobbzilla.wizard.model.HashedPassword; | |||
import org.cobbzilla.wizard.validation.ConstraintViolationBean; | |||
import org.glassfish.grizzly.http.server.Request; | |||
import org.glassfish.jersey.media.multipart.FormDataParam; | |||
import org.glassfish.jersey.server.ContainerRequest; | |||
@@ -49,9 +53,12 @@ import java.io.File; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.io.StringWriter; | |||
import java.util.List; | |||
import java.util.Locale; | |||
import static bubble.ApiConstants.*; | |||
import static bubble.model.account.Account.validatePassword; | |||
import static bubble.resources.account.AuthResource.forgotPasswordMessage; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.errorString; | |||
import static org.cobbzilla.util.http.HttpContentTypes.*; | |||
@@ -70,6 +77,7 @@ public class MeResource { | |||
@Autowired private AccountDownloadService downloadService; | |||
@Autowired private BubbleConfiguration configuration; | |||
@Autowired private AuthenticatorService authenticatorService; | |||
@Autowired private AccountMessageDAO messageDAO; | |||
@GET | |||
public Response me(@Context ContainerRequest ctx) { | |||
@@ -104,13 +112,34 @@ public class MeResource { | |||
} | |||
@POST @Path(EP_CHANGE_PASSWORD) | |||
public Response changePassword(@Context ContainerRequest ctx, | |||
public Response changePassword(@Context Request req, | |||
@Context ContainerRequest ctx, | |||
ChangePasswordRequest request) { | |||
final Account caller = userPrincipal(ctx); | |||
authenticatorService.ensureAuthenticated(ctx, ActionTarget.account); | |||
final AccountPolicy policy = policyDAO.findSingleByAccount(caller.getUuid()); | |||
if (policy != null && request.hasTotpToken()) { | |||
authenticatorService.authenticate(caller, policy, new AuthenticatorRequest() | |||
.setAccount(caller.getUuid()) | |||
.setAuthenticate(true) | |||
.setToken(request.getTotpToken())); | |||
} | |||
if (policy != null) authenticatorService.ensureAuthenticated(ctx, ActionTarget.account); | |||
if (!caller.getHashedPassword().isCorrectPassword(request.getOldPassword())) { | |||
return invalid("err.oldPassword.invalid", "old password was invalid"); | |||
return invalid("err.currentPassword.invalid", "current password was invalid", ""); | |||
} | |||
final ConstraintViolationBean passwordViolation = validatePassword(request.getNewPassword()); | |||
if (passwordViolation != null) return invalid(passwordViolation); | |||
if (policy != null) { | |||
final AccountMessage forgotPasswordMessage = forgotPasswordMessage(req, caller, configuration); | |||
final List<AccountContact> requiredApprovals = policy.getRequiredExternalApprovals(forgotPasswordMessage); | |||
if (!requiredApprovals.isEmpty()) { | |||
messageDAO.create(forgotPasswordMessage); | |||
return ok(caller.setMultifactorAuthList(requiredApprovals)); | |||
} | |||
} | |||
caller.setHashedPassword(new HashedPassword(request.getNewPassword())); | |||
// Update account, and write back to session | |||
@@ -259,8 +259,6 @@ public class StandardAccountMessageService implements AccountMessageService { | |||
.collect(Collectors.toList()); | |||
// return masked list of contacts remaining to approve | |||
return new Account().setMultifactorAuth(remainingApprovals.stream() | |||
.map(AccountContact::mask) | |||
.toArray(AccountContact[]::new)); | |||
return new Account().setMultifactorAuthList(remainingApprovals); | |||
} | |||
} |
@@ -156,6 +156,16 @@ button_label_create_account=Create Account | |||
button_label_delete_account=Delete | |||
button_label_force_delete_account=Force Delete | |||
# Change Password page | |||
form_title_change_password=Change Password | |||
field_label_current_password=Current Password | |||
field_label_new_password=New Password | |||
field_label_new_password_confirm=Confirm New Password | |||
button_label_change_password=Set New Password | |||
button_label_request_password_reset=Request Password Reset | |||
message_change_password_external_auth=Changing account password requires approval from these contacts on file: | |||
message_change_password_authenticator_auth=Changing account password requires Authenticator password | |||
# Networks table | |||
loading_networks=Loading bubbles... | |||
table_title_networks=Bubbles | |||
@@ -548,7 +558,7 @@ err.node.notInitialized=Node is not initialized | |||
err.node.running=Node must be stopped before deleting | |||
err.node.shutdownFailed=Node shutdown failed | |||
err.node.stop.error=Error stopping node | |||
err.oldPassword.invalid=Old password was invalid | |||
err.currentPassword.invalid=Current password was invalid | |||
err.paymentInfo.invalid=Payment information is invalid | |||
err.paymentInfo.required=Payment information is required | |||
err.paymentInfo.processingError=Processing payment information failed | |||
@@ -1 +1 @@ | |||
Subproject commit 7b0bc74bd5923465c6d99ea27ed7904c3078a9e5 | |||
Subproject commit 689734e6c9fa7a51cbe19b47105520d0801c47f0 |
@@ -1 +1 @@ | |||
Subproject commit 661578b96b67dd02c0e7540843ac8313bc6e8777 | |||
Subproject commit 1c4d29aef889254e046e781378e35012853d32a6 |