ソースを参照

change password working correctly for admins

tags/v0.5.0
Jonathan Cobb 5年前
コミット
57c48f2a6d
12個のファイルの変更319行の追加134行の削除
  1. +33
    -0
      bubble-server/src/main/java/bubble/filters/BubbleRateLimitFilter.java
  2. +31
    -11
      bubble-server/src/main/java/bubble/resources/account/AccountsResource.java
  3. +32
    -13
      bubble-server/src/main/java/bubble/resources/account/AuthResource.java
  4. +2
    -4
      bubble-server/src/main/java/bubble/resources/account/MeResource.java
  5. +3
    -3
      bubble-server/src/main/java/bubble/service/account/AuthenticatorService.java
  6. +10
    -1
      bubble-server/src/main/resources/bubble-config.yml
  7. +1
    -0
      bubble-server/src/test/java/bubble/test/AuthTest.java
  8. +199
    -0
      bubble-server/src/test/resources/models/tests/auth/change_admin_password.json
  9. +4
    -100
      bubble-server/src/test/resources/models/tests/auth/change_password.json
  10. +2
    -0
      bubble-server/src/test/resources/test-bubble-config.yml
  11. +1
    -1
      utils/cobbzilla-utils
  12. +1
    -1
      utils/cobbzilla-wizard

+ 33
- 0
bubble-server/src/main/java/bubble/filters/BubbleRateLimitFilter.java ファイルの表示

@@ -0,0 +1,33 @@
package bubble.filters;

import bubble.model.account.Account;
import lombok.NoArgsConstructor;
import org.cobbzilla.wizard.filters.RateLimitFilter;
import org.springframework.stereotype.Service;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.ext.Provider;
import java.security.Principal;
import java.util.List;

import static bubble.ApiConstants.SESSION_HEADER;

@Provider @Service @NoArgsConstructor
public class BubbleRateLimitFilter extends RateLimitFilter {

@Override protected String getToken(ContainerRequestContext request) { return request.getHeaderString(SESSION_HEADER); }

@Override protected List<String> getKeys(ContainerRequestContext request) {
return super.getKeys(request);
}

// super-admins have unlimited API usage. helpful when populating models
@Override protected boolean allowUnlimitedUse(Principal user) {
try {
return ((Account) user).admin();
} catch (Exception e) {
return false;
}
}

}

+ 31
- 11
bubble-server/src/main/java/bubble/resources/account/AccountsResource.java ファイルの表示

@@ -28,6 +28,7 @@ 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;
@@ -326,21 +327,42 @@ public class AccountsResource {
ChangePasswordRequest request) {
final AccountContext c = new AccountContext(ctx, id);
if (!c.caller.admin()) return forbidden();
final Account caller = accountDAO.findByUuid(c.caller.getUuid()).setToken(c.caller.getToken());

final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid());

if (c.caller.getUuid().equals(c.account.getUuid()) || c.account.admin()) {
if (policy != null) authenticatorService.ensureAuthenticated(ctx, policy, ActionTarget.account);
// if there was a totp token provided, try it
final String authSession;
if (request.hasTotpToken()) {
c.account.setToken(authenticatorService.authenticate(caller, policy, new AuthenticatorRequest()
.setAccount(c.account.getUuid())
.setAuthenticate(true)
.setToken(request.getTotpToken())));
} else {
authSession = null;
}

if (policy != null) {
// older admin, or self (since ctime would be equal)
final boolean olderAdmin = c.account.admin() && caller.getCtime() >= c.account.getCtime();

// admins created earlier can do anything to admins created later
// if admins created later try to change password of admins created earlier, we go through approvals
if (policy != null && olderAdmin) {
// ensure authenticator has been provided for account
authenticatorService.ensureAuthenticated(c.account, policy, ActionTarget.account);

// if caller is younger admin, they must know the current password of the admin whose password they're trying to change
if (!c.account.getHashedPassword().isCorrectPassword(request.getOldPassword())) {
return invalid("err.currentPassword.invalid", "current password was invalid", "");
}

final AccountMessage forgotPasswordMessage = forgotPasswordMessage(req, c.account, configuration);
final List<AccountContact> requiredApprovals = policy.getRequiredApprovals(forgotPasswordMessage);
final List<AccountContact> requiredExternalApprovals = policy.getRequiredExternalApprovals(forgotPasswordMessage);
if (!requiredApprovals.isEmpty()) {
if (requiredApprovals.stream().anyMatch(AccountContact::isAuthenticator)) {
if (!request.hasTotpToken()) return invalid("err.totpToken.required");
authenticatorService.authenticate(c.account, policy, new AuthenticatorRequest()
authenticatorService.authenticate(caller, policy, new AuthenticatorRequest()
.setAccount(c.account.getUuid())
.setAuthenticate(true)
.setToken(request.getTotpToken()));
@@ -352,16 +374,14 @@ public class AccountsResource {
}
}

if (!c.account.getHashedPassword().isCorrectPassword(request.getOldPassword())) {
return invalid("err.currentPassword.invalid", "current password was invalid", "");
}
// validate new password
final ConstraintViolationBean passwordViolation = validatePassword(request.getNewPassword());
if (passwordViolation != null) return invalid(passwordViolation);

// Update account
final Account updated = accountDAO.update(c.account);
if (c.caller.getUuid().equals(c.account.getUuid())) {
sessionDAO.update(c.caller.getApiToken(), updated);
// Set password, update account. Update session if caller is targeting self, otherwise invalidate all sessions
final Account updated = accountDAO.update(c.account.setHashedPassword(new HashedPassword(request.getNewPassword())));
if (caller.getUuid().equals(c.account.getUuid())) {
sessionDAO.update(caller.getApiToken(), updated);
} else {
sessionDAO.invalidateAllSessions(c.account.getUuid());
}


+ 32
- 13
bubble-server/src/main/java/bubble/resources/account/AuthResource.java ファイルの表示

@@ -237,25 +237,32 @@ public class AuthResource {
final AccountContact authenticator = authFactors.stream().filter(AccountContact::isAuthenticator).findFirst().orElse(null);
if (authenticator != null && request.hasTotpToken()) {
// try totp token now
authenticatorService.authenticate(account, policy, new AuthenticatorRequest()
account.setToken(authenticatorService.authenticate(account, policy, new AuthenticatorRequest()
.setAccount(account.getUuid())
.setAuthenticate(true)
.setToken(request.getTotpToken()));
.setToken(request.getTotpToken())));
authFactors.removeIf(AccountContact::isAuthenticator);
}
if (!empty(authFactors)) {
final AccountMessage loginRequest = accountMessageDAO.create(new AccountMessage()
.setAccount(account.getUuid())
.setNetwork(configuration.getThisNetwork().getUuid())
.setName(account.getUuid())
.setMessageType(AccountMessageType.request)
.setAction(AccountAction.login)
.setTarget(ActionTarget.account)
.setRemoteHost(getRemoteHost(req))
);
final AccountMessage loginRequest;
if (authFactors.size() == 1 && authFactors.get(0) == authenticator) {
// we have already authenticated, unless we didn't have a token
if (!request.hasTotpToken()) return invalid("err.totpToken.required");
loginRequest = null; // should never happen
} else {
loginRequest = accountMessageDAO.create(new AccountMessage()
.setAccount(account.getUuid())
.setNetwork(configuration.getThisNetwork().getUuid())
.setName(account.getUuid())
.setMessageType(AccountMessageType.request)
.setAction(AccountAction.login)
.setTarget(ActionTarget.account)
.setRemoteHost(getRemoteHost(req))
);
}
return ok(new Account()
.setName(account.getName())
.setLoginRequest(loginRequest.getUuid())
.setLoginRequest(loginRequest != null ? loginRequest.getUuid() : null)
.setMultifactorAuth(AccountContact.mask(authFactors)));
}
}
@@ -417,11 +424,23 @@ public class AuthResource {
final Account account = optionalUserPrincipal(ctx);
if (account == null) return invalid("err.logout.noSession");
if (all != null && all) {
sessionDAO.invalidateAllSessions(account.getApiToken());
sessionDAO.invalidateAllSessions(account.getUuid());
} else {
sessionDAO.invalidate(account.getApiToken());
}
return ok_empty();
}

@POST @Path(EP_LOGOUT+"/{id}")
public Response logoutUserEverywhere(@Context ContainerRequest ctx,
@PathParam("id") String id) {
final Account account = optionalUserPrincipal(ctx);
if (account == null) return invalid("err.logout.noSession");
if (!account.admin()) return forbidden();
final Account target = accountDAO.findById(id);
if (target == null) return notFound(id);
sessionDAO.invalidateAllSessions(id);
return ok_empty();
}

}

+ 2
- 4
bubble-server/src/main/java/bubble/resources/account/MeResource.java ファイルの表示

@@ -154,10 +154,8 @@ public class MeResource {
final ConstraintViolationBean passwordViolation = validatePassword(request.getNewPassword());
if (passwordViolation != null) return invalid(passwordViolation);

caller.setHashedPassword(new HashedPassword(request.getNewPassword()));

// Update account, and write back to session
final Account updated = accountDAO.update(caller);
// Set new password, update account, and write back to session
final Account updated = accountDAO.update(caller.setHashedPassword(new HashedPassword(request.getNewPassword())));
sessionDAO.update(caller.getApiToken(), updated);

return ok(updated);


+ 3
- 3
bubble-server/src/main/java/bubble/service/account/AuthenticatorService.java ファイルの表示

@@ -60,15 +60,15 @@ public class AuthenticatorService {
public void ensureAuthenticated(ContainerRequest ctx, ActionTarget target) {
final Account account = userPrincipal(ctx);
final AccountPolicy policy = policyDAO.findSingleByAccount(account.getUuid());
checkAuth(account, policy, target);
ensureAuthenticated(account, policy, target);
}

public void ensureAuthenticated(ContainerRequest ctx, AccountPolicy policy, ActionTarget target) {
final Account account = userPrincipal(ctx);
checkAuth(account, policy, target);
ensureAuthenticated(account, policy, target);
}

private void checkAuth(Account account, AccountPolicy policy, ActionTarget target) {
public void ensureAuthenticated(Account account, AccountPolicy policy, ActionTarget target) {
if (policy == null || !policy.hasVerifiedAuthenticator()) return;
if (target != null) {
final AccountContact authenticator = policy.getAuthenticator();


+ 10
- 1
bubble-server/src/main/resources/bubble-config.yml ファイルの表示

@@ -52,7 +52,9 @@ jersey:
- org.cobbzilla.wizard.filters
providerPackages:
- org.cobbzilla.wizard.exceptionmappers
requestFilters: [ bubble.auth.BubbleAuthFilter ]
requestFilters:
- bubble.auth.BubbleAuthFilter
- bubble.filters.BubbleRateLimitFilter
responseFilters:
- org.cobbzilla.wizard.filters.ScrubbableScrubber
- org.cobbzilla.wizard.filters.EntityTypeHeaderResponseFilter
@@ -72,3 +74,10 @@ letsencryptEmail: {{LETSENCRYPT_EMAIL}}
localStorageDir: {{LOCALSTORAGE_BASE_DIR}}

disallowedCountries: {{DISALLOWED_COUNTRIES}}

rateLimits:
- { limit: 200, interval: 3s, block: 5m }
- { limit: 10000, interval: 1m, block: 5m }
- { limit: 25000, interval: 10m, block: 1h }
- { limit: 100000, interval: 1h, block: 24h }
- { limit: 500000, interval: 6h, block: 96h }

+ 1
- 0
bubble-server/src/test/java/bubble/test/AuthTest.java ファイルの表示

@@ -26,6 +26,7 @@ public class AuthTest extends ActivatedBubbleModelTestBase {
@Test public void testRegistration () throws Exception { modelTest("auth/account_registration"); }
@Test public void testForgotPassword () throws Exception { modelTest("auth/forgot_password"); }
@Test public void testChangePassword () throws Exception { modelTest("auth/change_password"); }
@Test public void testChangeAdminPassword () throws Exception { modelTest("auth/change_admin_password"); }
@Test public void testMultifactorAuth () throws Exception { modelTest("auth/multifactor_auth"); }
@Test public void testDownloadAccount () throws Exception { modelTest("auth/download_account"); }
@Test public void testNetworkAuth () throws Exception { modelTest("auth/network_auth"); }


+ 199
- 0
bubble-server/src/test/resources/models/tests/auth/change_admin_password.json ファイルの表示

@@ -0,0 +1,199 @@
[

{
"comment": "get root policy (creates it)",
"request": { "uri": "users/root/policy" },
"response": {
"store": "rootPolicy"
}
},

{
"comment": "update root policy, add authenticator",
"include": "add_authenticator",
"params": {
"userId": "root",
"authenticatorVar": "rootAuthenticator"
}
},

{
"comment": "as root user, create another admin user",
"include": "new_account",
"params": {
"username": "admin-change_password",
"email": "admin-change_password@example.com",
"password": "bazquux1!",
"admin": true,
"verifyEmail": "true"
}
},

{
"comment": "login as second admin, verify OK",
"request": {
"session": "new",
"uri": "auth/login",
"entity": {
"name": "admin-change_password",
"password": "bazquux1!"
}
},
"response": {
"sessionName": "adminSession",
"session": "token"
}
},

{
"comment": "as second admin, read self, succeeds. verify we are admin",
"request": { "uri": "me" },
"response": {
"check": [
{"condition": "json.getName() === 'admin-change_password'"},
{"condition": "json.admin() === true"}
]
}
},

{
"comment": "as second admin, try to change root password without sending current password, fails without TOTP token",
"request": {
"uri": "users/root/changePassword",
"entity": {
"newPassword": "wh00pDeDoo."
}
},
"response": {
"status": 422,
"check": [ {"condition": "json.has('err.totpToken.invalid')"} ]
}
},

{
"comment": "as second admin, try to change root password sending TOTP token, but without sending current password, fails password check",
"request": {
"uri": "users/root/changePassword",
"entity": {
"newPassword": "wh00pDeDoo.",
"totpToken": "{{authenticator_token rootAuthenticator.totpKey}}"
}
},
"response": {
"status": 422,
"check": [ {"condition": "json.has('err.currentPassword.invalid')"} ]
}
},

{
"comment": "as admin user, try to change root user password sending current password and TOTP token, succeeds",
"request": {
"uri": "users/root/changePassword",
"session": "adminSession",
"entity": {
"oldPassword": "password",
"newPassword": "aNewRootPass1!",
"totpToken": "{{authenticator_token rootAuthenticator.totpKey}}"
}
}
},

{
"comment": "as root user, try to read self, fails because our session has been invalidated",
"request": {
"uri": "me",
"session": "rootSession"
},
"response": { "status": 403 }
},

{
"comment": "login as root with old password, verify failure",
"request": {
"session": "new",
"uri": "auth/login",
"entity": {
"name": "root",
"password": "password"
}
},
"response": {
"status": 404
}
},

{
"comment": "login as root with new password and TOTP token, succeeds",
"request": {
"session": "new",
"uri": "auth/login",
"entity": {
"name": "root",
"password": "aNewRootPass1!",
"totpToken": "{{authenticator_token rootAuthenticator.totpKey}}"
}
},
"response": {
"sessionName": "rootSession",
"session": "token"
}
},

{
"comment": "as root, read self, succeeds",
"request": { "uri": "me" },
"response": {
"check": [ {"condition": "json.getName() === 'root'"} ]
}
},

{
"comment": "as root user, try to change admin user password without sending current password, succeeds because we are senior admin",
"request": {
"uri": "users/admin-change_password/changePassword",
"entity": {
"newPassword": "newadminPASS1!"
}
}
},

{
"comment": "login as second admin with old password, verify failure",
"request": {
"session": "new",
"uri": "auth/login",
"entity": {
"name": "admin-change_password",
"password": "bazquux1!"
}
},
"response": {
"status": 404
}
},

{
"comment": "login as second admin with new password, verify OK",
"request": {
"session": "new",
"uri": "auth/login",
"entity": {
"name": "admin-change_password",
"password": "newadminPASS1!"
}
},
"response": {
"sessionName": "adminSession",
"session": "token"
}
},

{
"comment": "as second admin, read self, succeeds",
"request": { "uri": "me" },
"response": {
"check": [ {"condition": "json.getName() === 'admin-change_password'"} ]
}
}

]

+ 4
- 100
bubble-server/src/test/resources/models/tests/auth/change_password.json ファイルの表示

@@ -165,7 +165,10 @@

{
"comment": "as non-admin, read self-profile, succeeds",
"request": { "uri": "me" }
"request": { "uri": "me" },
"response": {
"check": [ {"condition": "json.getName() === 'user-change_password'"} ]
}
},

{
@@ -397,104 +400,5 @@
"totpToken": "{{authenticator_token userAuthenticator.totpKey}}"
}
}
},

{
"comment": "as root user, create another admin user",
"include": "new_account",
"params": {
"username": "admin-change_password",
"email": "admin-change_password@example.com",
"password": "bazquux1!",
"admin": true,
"verifyEmail": "true"
}
},

{
"comment": "login as second admin, verify OK",
"request": {
"session": "new",
"uri": "auth/login",
"entity": {
"name": "admin-change_password",
"password": "bazquux1!"
}
},
"response": {
"sessionName": "adminSession",
"session": "token"
}
},

{
"comment": "as second admin, read self-profile, succeeds. verify we are admin",
"request": { "uri": "me" },
"response": {
"check": [ {"condition": "json.admin() == true"} ]
}
},

{
"comment": "as root user, try to change admin user password without sending current password, fails",
"request": {
"uri": "users/admin-change_password/changePassword",
"session": "rootSession",
"entity": {
"newPassword": "newadminPASS!"
}
},
"response": {
"status": 422,
"check": [ {"condition": "json.has('err.currentPassword.invalid')"} ]
}
},

{
"comment": "as root user, try to change admin user password sending current password, succeeds",
"request": {
"uri": "users/admin-change_password/changePassword",
"session": "rootSession",
"entity": {
"oldPassword": "bazquux1!",
"newPassword": "newadminPASS2!"
}
}
},

{
"comment": "login as second admin with old password, verify failure",
"request": {
"session": "new",
"uri": "auth/login",
"entity": {
"name": "admin-change_password",
"password": "bazquux1!"
}
},
"response": {
"status": 404
}
},

{
"comment": "login as second admin with new password, verify OK",
"request": {
"session": "new",
"uri": "auth/login",
"entity": {
"name": "admin-change_password",
"password": "newadminPASS2!"
}
},
"response": {
"sessionName": "adminSession",
"session": "token"
}
},

{
"comment": "as second admin, read self-profile, succeeds",
"request": { "uri": "me" }
}
]

+ 2
- 0
bubble-server/src/test/resources/test-bubble-config.yml ファイルの表示

@@ -3,6 +3,8 @@

serverName: bubble-api

bcryptRounds: 2

#publicUriBase: https://127.0.0.1
publicUriBase: {{PUBLIC_BASE_URI}}



+ 1
- 1
utils/cobbzilla-utils

@@ -1 +1 @@
Subproject commit e7c3727fc3e1405b3bba6b95de0271db23309e14
Subproject commit 12d7d0d1104ee1c2da52f2348a96b49232e0ed3b

+ 1
- 1
utils/cobbzilla-wizard

@@ -1 +1 @@
Subproject commit 00e779f2fe37045ca2a82669bcf29c4b81f74525
Subproject commit 88f19d61844a4fd7bce11f81021e798473b9f135

読み込み中…
キャンセル
保存