Configurable Password Policy and 5 supporting JUnit tests.
Project: http://git-wip-us.apache.org/repos/asf/usergrid/repo Commit: http://git-wip-us.apache.org/repos/asf/usergrid/commit/a30e1a56 Tree: http://git-wip-us.apache.org/repos/asf/usergrid/tree/a30e1a56 Diff: http://git-wip-us.apache.org/repos/asf/usergrid/diff/a30e1a56 Branch: refs/heads/usergrid-1318-queue Commit: a30e1a564e74b033601db60d37960fca46b34fe7 Parents: c65f903 Author: Dave Johnson <snoopd...@apache.org> Authored: Wed Oct 19 13:42:34 2016 -0400 Committer: Dave Johnson <snoopd...@apache.org> Committed: Wed Oct 19 14:05:17 2016 -0400 ---------------------------------------------------------------------- .../exceptions/AbstractExceptionMapper.java | 2 +- .../PasswordPolicyViolationExceptionMapper.java | 48 ++++++ .../collection/users/PermissionsResourceIT.java | 4 +- .../collection/users/UserResourceIT.java | 38 ++++- .../usergrid/rest/management/AdminUsersIT.java | 51 ++++++ .../rest/management/ManagementResourceIT.java | 4 +- .../rest/management/RegistrationIT.java | 6 +- .../cassandra/ManagementServiceImpl.java | 73 +++++++-- .../usergrid/security/PasswordPolicy.java | 53 ++++++ .../usergrid/security/PasswordPolicyFig.java | 79 +++++++++ .../usergrid/security/PasswordPolicyImpl.java | 156 ++++++++++++++++++ .../PasswordPolicyViolationException.java | 46 ++++++ .../services/guice/ServiceModuleImpl.java | 8 + .../usergrid/security/PasswordPolicyTest.java | 47 ++++++ .../security/PasswordPolicyTestFig.java | 161 +++++++++++++++++++ 15 files changed, 750 insertions(+), 26 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/usergrid/blob/a30e1a56/stack/rest/src/main/java/org/apache/usergrid/rest/exceptions/AbstractExceptionMapper.java ---------------------------------------------------------------------- diff --git a/stack/rest/src/main/java/org/apache/usergrid/rest/exceptions/AbstractExceptionMapper.java b/stack/rest/src/main/java/org/apache/usergrid/rest/exceptions/AbstractExceptionMapper.java index 19d35fd..4a4b8b0 100644 --- a/stack/rest/src/main/java/org/apache/usergrid/rest/exceptions/AbstractExceptionMapper.java +++ b/stack/rest/src/main/java/org/apache/usergrid/rest/exceptions/AbstractExceptionMapper.java @@ -102,7 +102,7 @@ public abstract class AbstractExceptionMapper<E extends java.lang.Throwable> imp } - private Response toResponse( int status, String jsonResponse ) { + protected Response toResponse( int status, String jsonResponse ) { if ( status >= 500 ) { // only log real errors as errors logger.error( "Server Error ({}):\n{}", status, jsonResponse ); http://git-wip-us.apache.org/repos/asf/usergrid/blob/a30e1a56/stack/rest/src/main/java/org/apache/usergrid/rest/exceptions/PasswordPolicyViolationExceptionMapper.java ---------------------------------------------------------------------- diff --git a/stack/rest/src/main/java/org/apache/usergrid/rest/exceptions/PasswordPolicyViolationExceptionMapper.java b/stack/rest/src/main/java/org/apache/usergrid/rest/exceptions/PasswordPolicyViolationExceptionMapper.java new file mode 100644 index 0000000..fcd09e3 --- /dev/null +++ b/stack/rest/src/main/java/org/apache/usergrid/rest/exceptions/PasswordPolicyViolationExceptionMapper.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.usergrid.rest.exceptions; + + +import org.apache.usergrid.rest.ApiResponse; +import org.apache.usergrid.services.exceptions.PasswordPolicyViolationException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static org.apache.usergrid.utils.JsonUtils.mapToJsonString; + + +/** <p> Mapper for OAuthProblemException. </p> */ +@Provider +public class PasswordPolicyViolationExceptionMapper extends AbstractExceptionMapper<PasswordPolicyViolationException> { + + @Override + public Response toResponse( PasswordPolicyViolationException e ) { + + ApiResponse apiResponse = new ApiResponse(); + apiResponse.setError( e.getMessage() ); + + StringBuilder sb = new StringBuilder(); + for ( String violation : e.getViolations() ) { + sb.append( violation ).append(" "); + } + apiResponse.setErrorDescription( sb.toString() ); + + return toResponse( SC_BAD_REQUEST, mapToJsonString(apiResponse) ); + } +} http://git-wip-us.apache.org/repos/asf/usergrid/blob/a30e1a56/stack/rest/src/test/java/org/apache/usergrid/rest/applications/collection/users/PermissionsResourceIT.java ---------------------------------------------------------------------- diff --git a/stack/rest/src/test/java/org/apache/usergrid/rest/applications/collection/users/PermissionsResourceIT.java b/stack/rest/src/test/java/org/apache/usergrid/rest/applications/collection/users/PermissionsResourceIT.java index 5380e00..aff952b 100644 --- a/stack/rest/src/test/java/org/apache/usergrid/rest/applications/collection/users/PermissionsResourceIT.java +++ b/stack/rest/src/test/java/org/apache/usergrid/rest/applications/collection/users/PermissionsResourceIT.java @@ -638,12 +638,12 @@ public class PermissionsResourceIT extends AbstractRestIT { // cannot create app user named me try { - app().collection( "users" ).post( new User( "me", "it's me", "m...@example.com", "me!" ) ); + app().collection( "users" ).post( new User( "me", "it's me", "m...@example.com", "me!me!" ) ); fail("Must not be able to create app user named me"); } catch ( BadRequestException expected ) {} // cannot use update to rename app user to me - Entity user = app().collection( "users" ).post( new User( "dave", "Sneaky Me", "m...@example.com", "me!" ) ); + Entity user = app().collection( "users" ).post( new User( "dave", "Sneaky Me", "m...@example.com", "me!me!" ) ); try { app().collection( "users" ).entity( user ).put( new Entity().chainPut( "username", "me" )); fail("Must not be able to update app user to name me"); http://git-wip-us.apache.org/repos/asf/usergrid/blob/a30e1a56/stack/rest/src/test/java/org/apache/usergrid/rest/applications/collection/users/UserResourceIT.java ---------------------------------------------------------------------- diff --git a/stack/rest/src/test/java/org/apache/usergrid/rest/applications/collection/users/UserResourceIT.java b/stack/rest/src/test/java/org/apache/usergrid/rest/applications/collection/users/UserResourceIT.java index d36d0a1..c9dc0d8 100644 --- a/stack/rest/src/test/java/org/apache/usergrid/rest/applications/collection/users/UserResourceIT.java +++ b/stack/rest/src/test/java/org/apache/usergrid/rest/applications/collection/users/UserResourceIT.java @@ -47,6 +47,7 @@ import java.io.IOException; import javax.ws.rs.core.MediaType; +import static org.apache.usergrid.security.PasswordPolicy.ERROR_POLICY_VIOLIATION; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -776,6 +777,7 @@ public class UserResourceIT extends AbstractRestIT { } + @Test public void test_PUT_password_ok() { Entity entity = usersResource.post(new User("edanuff", "edanuff", "edan...@email.com", "sesame")); @@ -798,7 +800,7 @@ public class UserResourceIT extends AbstractRestIT { @Test public void setUserPasswordAsAdmin() throws IOException { usersResource.post(new User("edanuff", "edanuff", "edan...@email.com", "sesame")); - String newPassword = "foo"; + String newPassword = "foofoo"; refreshIndex(); // change the password as admin. The old password isn't required @@ -816,8 +818,8 @@ public class UserResourceIT extends AbstractRestIT { public void passwordMismatchErrorUser() { usersResource.post(new User("edanuff", "edanuff", "edan...@email.com", "sesame")); - String origPassword = "foo"; - String newPassword = "bar"; + String origPassword = "foofoo"; + String newPassword = "barbar"; ChangePasswordEntity data = new ChangePasswordEntity(origPassword, newPassword); @@ -833,6 +835,36 @@ public class UserResourceIT extends AbstractRestIT { @Test + public void createAppUserWithInvalidPassword() { + + try { + Entity entity = usersResource.post(new User("edanuff", "edanuff", "edan...@email.com", "foo")); + fail("Invalid password should have caused error"); + + } catch( ClientErrorException uie ) { + errorParse( 400, ERROR_POLICY_VIOLIATION, uie ); + } + } + + + @Test + public void testChangePassordToInvalidValue() { + + Entity entity = usersResource.post(new User("edanuff", "edanuff", "edan...@email.com", "sesame")); + refreshIndex(); + + try { + usersResource.entity(entity).collection("password").post(new ChangePasswordEntity("sesame", "abc")); + fail("Invalid password should have caused error"); + + } catch( ClientErrorException uie ) { + errorParse( 400, ERROR_POLICY_VIOLIATION, uie ); + } + + } + + + @Test public void addRemoveRole() throws IOException { String roleName = "rolename"; http://git-wip-us.apache.org/repos/asf/usergrid/blob/a30e1a56/stack/rest/src/test/java/org/apache/usergrid/rest/management/AdminUsersIT.java ---------------------------------------------------------------------- diff --git a/stack/rest/src/test/java/org/apache/usergrid/rest/management/AdminUsersIT.java b/stack/rest/src/test/java/org/apache/usergrid/rest/management/AdminUsersIT.java index a8bd834..f80f131 100644 --- a/stack/rest/src/test/java/org/apache/usergrid/rest/management/AdminUsersIT.java +++ b/stack/rest/src/test/java/org/apache/usergrid/rest/management/AdminUsersIT.java @@ -19,6 +19,7 @@ package org.apache.usergrid.rest.management; import com.sun.jersey.api.client.UniformInterfaceException; import net.jcip.annotations.NotThreadSafe; +import org.apache.commons.lang.RandomStringUtils; import org.apache.usergrid.management.MockImapClient; import org.apache.usergrid.persistence.core.util.StringUtils; import org.apache.usergrid.persistence.index.utils.UUIDUtils; @@ -42,6 +43,7 @@ import java.io.IOException; import java.util.*; import static org.apache.usergrid.management.AccountCreationProps.*; +import static org.apache.usergrid.security.PasswordPolicy.ERROR_POLICY_VIOLIATION; import static org.junit.Assert.*; @@ -92,6 +94,55 @@ public class AdminUsersIT extends AbstractRestIT { /** + * Test that creating user with password that violates policy results in informative error message. + */ + @Test + public void createUserWithInvalidPassword() throws IOException { + + String rando = RandomStringUtils.randomAlphanumeric(10); + Form userForm = new Form(); + userForm.param( "username", "user_" + rando ); + userForm.param( "name", rando); + userForm.param( "email", "user_" + rando + "@example.com" ); + userForm.param( "password", "abc" ); + + try { + management().users().post( User.class, userForm ); + fail("Invalid password should have caused error"); + + } catch( ClientErrorException uie ) { + errorParse( 400, ERROR_POLICY_VIOLIATION, uie ); + } + + } + + + /** + * Test that setting a password that violates policy results in informative error message. + */ + @Test + public void resetPasswordWithInvalidNewPassword() throws IOException { + + String username = clientSetup.getUsername(); + String password = clientSetup.getPassword(); + + Map<String, Object> passwordPayload = new HashMap<String, Object>(); + + // Default password policy is lenient, only requires length of 4 + passwordPayload.put( "newpassword", "abc" ); + passwordPayload.put( "oldpassword", password ); + + try { + management.users().user( username ).password().post( Entity.class, passwordPayload ); + fail("Invalid password should have caused error"); + + } catch( ClientErrorException uie ) { + errorParse( 400, ERROR_POLICY_VIOLIATION, uie ); + } + } + + + /** * Check that we cannot change the password by using an older password */ @Test http://git-wip-us.apache.org/repos/asf/usergrid/blob/a30e1a56/stack/rest/src/test/java/org/apache/usergrid/rest/management/ManagementResourceIT.java ---------------------------------------------------------------------- diff --git a/stack/rest/src/test/java/org/apache/usergrid/rest/management/ManagementResourceIT.java b/stack/rest/src/test/java/org/apache/usergrid/rest/management/ManagementResourceIT.java index 1a3eb1d..635368e 100644 --- a/stack/rest/src/test/java/org/apache/usergrid/rest/management/ManagementResourceIT.java +++ b/stack/rest/src/test/java/org/apache/usergrid/rest/management/ManagementResourceIT.java @@ -81,7 +81,7 @@ public class ManagementResourceIT extends AbstractRestIT { .users() .post( ApiResponse.class, new User( "test" + uuid, "test" + uuid, "test" + uuid + "@email.com", "test" ) ); Map<String, Object> data = new HashMap<>(); - data.put( "newpassword", "foo" ); + data.put( "newpassword", "foofoo" ); data.put( "oldpassword", "test" ); management.users() .user( "test" + uuid ) @@ -90,7 +90,7 @@ public class ManagementResourceIT extends AbstractRestIT { Token token = management.token().post(Token.class, new Token( "test"+uuid, "foo" ) ); management.token().setToken( token ); data.clear(); - data.put( "oldpassword", "foo" ); + data.put( "oldpassword", "foofoo" ); data.put( "newpassword", "test" ); management.users().user("test"+uuid).password().post(Entity.class, data); } http://git-wip-us.apache.org/repos/asf/usergrid/blob/a30e1a56/stack/rest/src/test/java/org/apache/usergrid/rest/management/RegistrationIT.java ---------------------------------------------------------------------- diff --git a/stack/rest/src/test/java/org/apache/usergrid/rest/management/RegistrationIT.java b/stack/rest/src/test/java/org/apache/usergrid/rest/management/RegistrationIT.java index 885710d..8404632 100644 --- a/stack/rest/src/test/java/org/apache/usergrid/rest/management/RegistrationIT.java +++ b/stack/rest/src/test/java/org/apache/usergrid/rest/management/RegistrationIT.java @@ -145,6 +145,7 @@ public class RegistrationIT extends AbstractRestIT { /** * Test checking that we should be able to add a admin with no password attached to them. + * * @throws Exception */ @@ -163,7 +164,10 @@ public class RegistrationIT extends AbstractRestIT { // this should send resetpwd link in email to newly added org admin user(that did not exist ///in usergrid) and "User Invited To Organization" email String adminToken = getAdminToken().getAccessToken(); - Entity node = postAddAdminToOrg(this.clientSetup.getOrganizationName(), this.clientSetup.getUsername()+"@servertest.com", ""); + Entity node = postAddAdminToOrg( + this.clientSetup.getOrganizationName(), + this.clientSetup.getUsername()+"@servertest.com", + "changeme"); UUID userId = node.getUuid(); refreshIndex(); http://git-wip-us.apache.org/repos/asf/usergrid/blob/a30e1a56/stack/services/src/main/java/org/apache/usergrid/management/cassandra/ManagementServiceImpl.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/management/cassandra/ManagementServiceImpl.java b/stack/services/src/main/java/org/apache/usergrid/management/cassandra/ManagementServiceImpl.java index 03377a3..35fdd30 100644 --- a/stack/services/src/main/java/org/apache/usergrid/management/cassandra/ManagementServiceImpl.java +++ b/stack/services/src/main/java/org/apache/usergrid/management/cassandra/ManagementServiceImpl.java @@ -54,6 +54,7 @@ import org.apache.usergrid.persistence.model.entity.Id; import org.apache.usergrid.persistence.model.entity.SimpleId; import org.apache.usergrid.security.AuthPrincipalInfo; import org.apache.usergrid.security.AuthPrincipalType; +import org.apache.usergrid.security.PasswordPolicy; import org.apache.usergrid.security.crypto.EncryptionService; import org.apache.usergrid.security.oauth.AccessInfo; import org.apache.usergrid.security.oauth.ClientCredentialsInfo; @@ -70,6 +71,7 @@ import org.apache.usergrid.security.tokens.TokenInfo; import org.apache.usergrid.security.tokens.TokenService; import org.apache.usergrid.security.tokens.exceptions.TokenException; import org.apache.usergrid.services.*; +import org.apache.usergrid.services.exceptions.PasswordPolicyViolationException; import org.apache.usergrid.utils.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -172,6 +174,8 @@ public class ManagementServiceImpl implements ManagementService { protected LocalShiroCache localShiroCache; + protected PasswordPolicy passwordPolicy; + private LoadingCache<UUID, OrganizationConfig> orgConfigByAppCache = CacheBuilder.newBuilder().maximumSize( 1000 ) .expireAfterWrite( Long.valueOf( System.getProperty(ORG_CONFIG_CACHE_PROP, "30000") ) , TimeUnit.MILLISECONDS) @@ -215,6 +219,8 @@ public class ManagementServiceImpl implements ManagementService { this.service = injector.getInstance(ApplicationService.class); this.localShiroCache = injector.getInstance(LocalShiroCache.class); + this.passwordPolicy = injector.getInstance( PasswordPolicy.class ); + } @Autowired @@ -929,7 +935,8 @@ public class ManagementServiceImpl implements ManagementService { @Override - public UserInfo createAdminFromPrexistingPassword( UUID organizationId, User user, CredentialsInfo ci ) throws Exception { + public UserInfo createAdminFromPrexistingPassword( UUID organizationId, User user, CredentialsInfo ci ) + throws Exception { return doCreateAdmin( organizationId, user, ci, // we can't actually set the mongo password. We never have the plain text in @@ -941,6 +948,12 @@ public class ManagementServiceImpl implements ManagementService { @Override public UserInfo createAdminFrom( UUID organizationId, User user, String password ) throws Exception { + + Collection<String> policyVioliations = passwordPolicy.policyCheck( password, false ); + if ( !policyVioliations.isEmpty() ) { + throw new PasswordPolicyViolationException( passwordPolicy.getDescription( true ), policyVioliations ); + } + return doCreateAdmin(organizationId, user, encryptionService.defaultEncryptedCredentials(password, user.getUuid(), smf.getManagementAppId()), encryptionService.plainTextCredentials(mongoPassword(user.getUsername(), password), user.getUuid(), @@ -949,20 +962,23 @@ public class ManagementServiceImpl implements ManagementService { @Override - public UserInfo createAdminUser( UUID organizationId, String username, String name, String email, String password, boolean activated, + public UserInfo createAdminUser( UUID organizationId, String username, String name, String email, + String password, boolean activated, boolean disabled ) throws Exception { return createAdminUser(organizationId, username, name, email, password, activated, disabled, null); } @Override - public UserInfo createAdminUser( UUID organizationId, String username, String name, String email, String password, boolean activated, - boolean disabled, Map<String, Object> userProperties ) throws Exception { + public UserInfo createAdminUser( UUID organizationId, String username, String name, String email, String password, + boolean activated, boolean disabled, Map<String, Object> userProperties ) + throws Exception { if ( !validateAdminInfo(username, name, email, password) ) { return null; } - return createAdminUserInternal( organizationId, username, name, email, password, activated, disabled, userProperties ); + return createAdminUserInternal( + organizationId, username, name, email, password, activated, disabled, userProperties ); } @@ -976,25 +992,32 @@ public class ManagementServiceImpl implements ManagementService { EntityManager em = emf.getEntityManager( smf.getManagementAppId() ); - if ( !( tokens.isExternalSSOProviderEnabled() && SubjectUtils.isServiceAdmin()) && !em.isPropertyValueUniqueForEntity( "user", "username", username ) ) { + if ( !( tokens.isExternalSSOProviderEnabled() && SubjectUtils.isServiceAdmin()) + && !em.isPropertyValueUniqueForEntity( "user", "username", username ) ) { throw new DuplicateUniquePropertyExistsException( "user", "username", username ); } - if ( !(tokens.isExternalSSOProviderEnabled()&& SubjectUtils.isServiceAdmin()) && !em.isPropertyValueUniqueForEntity( "user", "email", email ) ) { + if ( !(tokens.isExternalSSOProviderEnabled()&& SubjectUtils.isServiceAdmin()) + && !em.isPropertyValueUniqueForEntity( "user", "email", email ) ) { throw new DuplicateUniquePropertyExistsException( "user", "email", email ); } return true; } - protected UserInfo createAdminUserInternal( UUID organizationId, String username, String name, String email, String password, - boolean activated, boolean disabled, Map<String, Object> userProperties ) + protected UserInfo createAdminUserInternal( UUID organizationId, String username, String name, String email, + String password, boolean activated, boolean disabled, + Map<String, Object> userProperties ) throws Exception { + + logger.info( "createAdminUserInternal: {}", username ); - if ( isBlank( password ) ) { - password = encodeBase64URLSafeString( bytes( UUID.randomUUID() ) ); + Collection<String> policyVioliations = passwordPolicy.policyCheck( password, true ); + if ( !policyVioliations.isEmpty() ) { + throw new PasswordPolicyViolationException( passwordPolicy.getDescription( true ), policyVioliations ); } + if ( username == null ) { username = email; } @@ -1265,6 +1288,11 @@ public class ManagementServiceImpl implements ManagementService { return; } + Collection<String> policyVioliations = passwordPolicy.policyCheck( newPassword, true ); + if ( !policyVioliations.isEmpty() ) { + throw new PasswordPolicyViolationException( passwordPolicy.getDescription( true ), policyVioliations ); + } + EntityManager em = emf.getEntityManager( smf.getManagementAppId() ); User user = em.get( userId, User.class ); @@ -1646,7 +1674,8 @@ public class ManagementServiceImpl implements ManagementService { @Override - public Map<String, Object> getAdminUserOrganizationData(UserInfo user, boolean includeApps, boolean includeOrgUsers) throws Exception { + public Map<String, Object> getAdminUserOrganizationData(UserInfo user, boolean includeApps, boolean includeOrgUsers) + throws Exception { Map<String, Object> json = new HashMap<>(); @@ -2654,8 +2683,8 @@ public class ManagementServiceImpl implements ManagementService { String token = getConfirmationTokenForAdminUser(user.getUuid(), 0, organizationId); OrganizationConfig orgConfig = organizationId != null ? getOrganizationConfigByUuid(organizationId) : getOrganizationConfigForUserInfo(user); - String confirmation_url = orgConfig.getFullUrl(WorkflowUrl.ADMIN_CONFIRMATION_URL, user.getUuid().toString()) + - "?token=" + token; + String confirmation_url = orgConfig.getFullUrl(WorkflowUrl.ADMIN_CONFIRMATION_URL, + user.getUuid().toString()) + "?token=" + token; sendAdminUserEmail(user, "User Account Confirmation: " + user.getEmail(), emailMsg(hashMap("confirm_email", user.getEmail()).map("confirmation_url", confirmation_url), PROPERTIES_EMAIL_ADMIN_CONFIRMATION)); @@ -2779,7 +2808,8 @@ public class ManagementServiceImpl implements ManagementService { @Override - public boolean checkPasswordResetTokenForAppUser( UUID applicationId, UUID userId, String token ) throws Exception { + public boolean checkPasswordResetTokenForAppUser( UUID applicationId, UUID userId, String token ) + throws Exception { AuthPrincipalInfo principal = null; try { principal = getPrincipalFromAccessToken( token, TOKEN_TYPE_PASSWORD_RESET, APPLICATION_USER ); @@ -3036,10 +3066,16 @@ public class ManagementServiceImpl implements ManagementService { @Override public void setAppUserPassword( UUID applicationId, UUID userId, String newPassword ) throws Exception { + if ( ( userId == null ) || ( newPassword == null ) ) { return; } + Collection<String> policyVioliations = passwordPolicy.policyCheck( newPassword, false ); + if ( !policyVioliations.isEmpty() ) { + throw new PasswordPolicyViolationException( passwordPolicy.getDescription( false ), policyVioliations ); + } + EntityManager em = emf.getEntityManager( applicationId ); User user = em.get(userId, User.class); @@ -3051,6 +3087,7 @@ public class ManagementServiceImpl implements ManagementService { @Override public void setAppUserPassword( UUID applicationId, UUID userId, String oldPassword, String newPassword ) throws Exception { + if ( ( userId == null ) ) { throw new IllegalArgumentException( "userId is required" ); } @@ -3097,7 +3134,8 @@ public class ManagementServiceImpl implements ManagementService { final CredentialsInfo ci = readUserPasswordCredentials( applicationId, userId, User.ENTITY_TYPE ); if ( ci == null ) { - throw new EntityNotFoundException("Could not find credentials for user with id " + userId + " in application" + applicationId ); + throw new EntityNotFoundException("Could not find credentials for user with id " + userId + + " in application" + applicationId ); } return ci; @@ -3242,7 +3280,8 @@ public class ManagementServiceImpl implements ManagementService { /** read the user password credential's info */ - protected CredentialsInfo readUserPasswordCredentials( UUID appId, UUID ownerId, String ownerType ) throws Exception { + protected CredentialsInfo readUserPasswordCredentials( UUID appId, UUID ownerId, String ownerType ) + throws Exception { return readCreds( appId, ownerId, ownerType, USER_PASSWORD ); } http://git-wip-us.apache.org/repos/asf/usergrid/blob/a30e1a56/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicy.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicy.java b/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicy.java new file mode 100644 index 0000000..cc29b20 --- /dev/null +++ b/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicy.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.usergrid.security; + + +import java.util.Collection; + + +/** + * Interface to password policy. + */ +public interface PasswordPolicy { + + String ERROR_POLICY_VIOLIATION = "error_password_policy_violation"; + + String ERROR_UPPERCASE_POLICY = "error_uppercase_policy"; + + String ERROR_DIGITS_POLICY = "error_digits_policy"; + + String ERROR_SPECIAL_CHARS_POLICY = "error_special_chars_policy"; + + String ERROR_LENGTH_POLICY = "error_length_policy"; + + + /** + * Check to see if password conforms to policy. + * + * @param password Password to check. + * @return Collection of error strings, one for each policy violated or empty if password conforms. + */ + Collection<String> policyCheck( String password, boolean isAdminUser ); + + + /** + * Get description of password policy for error messages. + */ + String getDescription( boolean isAdminUser ); +} http://git-wip-us.apache.org/repos/asf/usergrid/blob/a30e1a56/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicyFig.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicyFig.java b/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicyFig.java new file mode 100644 index 0000000..41cda7c --- /dev/null +++ b/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicyFig.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.usergrid.security; + +import org.safehaus.guicyfig.Default; +import org.safehaus.guicyfig.FigSingleton; +import org.safehaus.guicyfig.GuicyFig; +import org.safehaus.guicyfig.Key; + + +@FigSingleton +public interface PasswordPolicyFig extends GuicyFig { + + String ALLOWED_SPECIAL_CHARS = "usergrid.password-policy.allowed-special-chars"; + + String MIN_UPPERCASE_ADMIN = "usergrid.password-policy.min-uppercase-admin"; + String MIN_UPPERCASE = "usergrid.password-policy.min-uppercase"; + + String MIN_DIGITS_ADMIN = "usergrid.password-policy.min-uppercase-admin"; + String MIN_DIGITS = "usergrid.password-policy.min-uppercase"; + + String MIN_SPECIAL_CHARS_ADMIN = "usergrid.password-policy.min-special-chars-admin"; + String MIN_SPECIAL_CHARS = "usergrid.password-policy.min-special-chars"; + + String MIN_LENGTH_ADMIN = "usergrid.password-policy.min-length-admin"; + String MIN_LENGTH = "usergrid.password-policy.min-length"; + + + @Key(MIN_UPPERCASE_ADMIN) + @Default("0") + int getMinUppercaseAdmin(); + + @Key(MIN_UPPERCASE) + @Default("0") + int getMinUppercase(); + + @Key(MIN_DIGITS_ADMIN) + @Default("0") + int getMinDigitsAdmin(); + + @Key(MIN_DIGITS) + @Default("0") + int getMinDigits(); + + @Key(MIN_SPECIAL_CHARS_ADMIN) + @Default("0") + int getMinSpecialCharsAdmin(); + + @Key(MIN_SPECIAL_CHARS) + @Default("0") + int getMinSpecialChars(); + + @Key(MIN_LENGTH_ADMIN) + @Default("4") + int getMinLengthAdmin(); + + @Key(MIN_LENGTH) + @Default("4") + int getMinLength(); + + @Key(ALLOWED_SPECIAL_CHARS) + @Default("`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?") + String getAllowedSpecialChars(); +} http://git-wip-us.apache.org/repos/asf/usergrid/blob/a30e1a56/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicyImpl.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicyImpl.java b/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicyImpl.java new file mode 100644 index 0000000..500592a --- /dev/null +++ b/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicyImpl.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.usergrid.security; + +import com.google.inject.Inject; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + + +public class PasswordPolicyImpl implements PasswordPolicy { + + private final PasswordPolicyFig passwordPolicyFig; + + + @Inject + PasswordPolicyImpl( PasswordPolicyFig passwordPolicyFig ) { + this.passwordPolicyFig = passwordPolicyFig; + } + + + @Override + public String getDescription( boolean isAdminUser ) { + + final int minLength; + final int minUppercase; + final int minDigits; + final int minSpecialChars; + + if ( isAdminUser ) { + minLength = passwordPolicyFig.getMinLengthAdmin(); + minUppercase = passwordPolicyFig.getMinUppercaseAdmin(); + minDigits = passwordPolicyFig.getMinDigitsAdmin(); + minSpecialChars = passwordPolicyFig.getMinSpecialCharsAdmin(); + } else { + minLength = passwordPolicyFig.getMinLength(); + minUppercase = passwordPolicyFig.getMinUppercase(); + minDigits = passwordPolicyFig.getMinDigits(); + minSpecialChars = passwordPolicyFig.getMinSpecialChars(); + } + + StringBuilder sb = new StringBuilder(); + sb.append( "Password must be at least " ).append( minLength ).append(" characters. "); + if ( minUppercase > 0 ) { + sb.append( "Must include " ).append( minUppercase ).append(" uppercase characters. "); + } + if ( minDigits > 0 ) { + sb.append( "Must include " ).append( minDigits ).append(" numbers. "); + } + if ( minSpecialChars > 0 ) { + sb.append( "Must include " ).append( minUppercase ).append(" special characters. "); + } + return sb.toString(); + } + + + @Override + public Collection<String> policyCheck( String password, boolean isAdminUser ) { + + final int minLength; + final int minUppercase; + final int minDigits; + final int minSpecialChars; + + if ( isAdminUser ) { + minLength = passwordPolicyFig.getMinLengthAdmin(); + minUppercase = passwordPolicyFig.getMinUppercaseAdmin(); + minDigits = passwordPolicyFig.getMinDigitsAdmin(); + minSpecialChars = passwordPolicyFig.getMinSpecialCharsAdmin(); + } else { + minLength = passwordPolicyFig.getMinLength(); + minUppercase = passwordPolicyFig.getMinUppercase(); + minDigits = passwordPolicyFig.getMinDigits(); + minSpecialChars = passwordPolicyFig.getMinSpecialChars(); + } + + return policyCheck( password, minLength, minUppercase, minDigits, minSpecialChars ); + } + + + public Collection<String> policyCheck( + String password, int minLength, int minUppercase, int minDigits, int minSpecialChars ) { + + + List<String> violations = new ArrayList<>(3); + + // check length + if ( password == null || password.length() < minLength ) { + violations.add( PasswordPolicy.ERROR_LENGTH_POLICY + + ": must be at least " + minLength + " characters" ); + } + + // count upper case + if ( minUppercase > 0 ) { + int upperCaseCount = 0; + for (char c : password.toCharArray()) { + if (StringUtils.isAllUpperCase( String.valueOf( c ) )) { + upperCaseCount++; + } + } + if (upperCaseCount < minUppercase) { + violations.add( PasswordPolicy.ERROR_UPPERCASE_POLICY + + ": requires " + minUppercase + " uppercase characters" ); + } + } + + // count digits case + if ( minDigits > 0 ) { + int digitCount = 0; + for (char c : password.toCharArray()) { + if (StringUtils.isNumeric( String.valueOf( c ) )) { + digitCount++; + } + } + if (digitCount < minDigits) { + violations.add( PasswordPolicy.ERROR_DIGITS_POLICY + + ": requires " + minDigits + " digits" ); + } + } + + // count special characters + if ( minSpecialChars > 0 ) { + int specialCharCount = 0; + for (char c : password.toCharArray()) { + if (passwordPolicyFig.getAllowedSpecialChars().contains( String.valueOf( c ) )) { + specialCharCount++; + } + } + if (specialCharCount < minSpecialChars) { + violations.add( PasswordPolicy.ERROR_SPECIAL_CHARS_POLICY + + ": requires " + minSpecialChars + " special characters" ); + } + } + + return violations; + } + + +} http://git-wip-us.apache.org/repos/asf/usergrid/blob/a30e1a56/stack/services/src/main/java/org/apache/usergrid/services/exceptions/PasswordPolicyViolationException.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/services/exceptions/PasswordPolicyViolationException.java b/stack/services/src/main/java/org/apache/usergrid/services/exceptions/PasswordPolicyViolationException.java new file mode 100644 index 0000000..531e3fd --- /dev/null +++ b/stack/services/src/main/java/org/apache/usergrid/services/exceptions/PasswordPolicyViolationException.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.usergrid.services.exceptions; + + +import java.util.Collection; + +import static org.apache.usergrid.security.PasswordPolicy.ERROR_POLICY_VIOLIATION; + + +public class PasswordPolicyViolationException extends RuntimeException { + private static final long serialVersionUID = 1L; + + private final Collection<String> violations; + private final String description; + + public PasswordPolicyViolationException( String description, Collection<String> violations ) { + super( ERROR_POLICY_VIOLIATION ); + this.violations = violations; + this.description = description; + } + + + public Collection<String> getViolations() { + return violations; + } + + + public String getDescription() { + return description; + } +} http://git-wip-us.apache.org/repos/asf/usergrid/blob/a30e1a56/stack/services/src/main/java/org/apache/usergrid/services/guice/ServiceModuleImpl.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/services/guice/ServiceModuleImpl.java b/stack/services/src/main/java/org/apache/usergrid/services/guice/ServiceModuleImpl.java index 58b301a..c7f7d08 100644 --- a/stack/services/src/main/java/org/apache/usergrid/services/guice/ServiceModuleImpl.java +++ b/stack/services/src/main/java/org/apache/usergrid/services/guice/ServiceModuleImpl.java @@ -24,6 +24,7 @@ import com.google.inject.TypeLiteral; import com.google.inject.multibindings.Multibinder; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.usergrid.corepersistence.ServiceModule; +import org.apache.usergrid.corepersistence.index.CoreIndexFig; import org.apache.usergrid.locking.guice.LockModule; import org.apache.usergrid.management.AppInfoMigrationPlugin; import org.apache.usergrid.persistence.cache.CacheFactory; @@ -31,8 +32,12 @@ import org.apache.usergrid.persistence.cache.impl.CacheFactoryImpl; import org.apache.usergrid.persistence.cache.impl.ScopedCacheSerialization; import org.apache.usergrid.persistence.cache.impl.ScopedCacheSerializationImpl; import org.apache.usergrid.persistence.core.migration.data.MigrationPlugin; +import org.apache.usergrid.security.PasswordPolicy; +import org.apache.usergrid.security.PasswordPolicyFig; +import org.apache.usergrid.security.PasswordPolicyImpl; import org.apache.usergrid.security.shiro.UsergridAuthenticationInfo; import org.apache.usergrid.security.shiro.UsergridAuthorizationInfo; +import org.safehaus.guicyfig.GuicyFigModule; // <bean id="notificationsQueueListener" class="org.apache.usergrid.services.notifications.QueueListener" @@ -70,5 +75,8 @@ public class ServiceModuleImpl extends AbstractModule implements ServiceModule { bind( new TypeLiteral<ScopedCacheSerialization<String, UsergridAuthenticationInfo>>() {}) .to( new TypeLiteral<ScopedCacheSerializationImpl<String, UsergridAuthenticationInfo>>() {}); + bind( PasswordPolicy.class ).to( PasswordPolicyImpl.class ); + + install( new GuicyFigModule( PasswordPolicyFig.class ) ); } } http://git-wip-us.apache.org/repos/asf/usergrid/blob/a30e1a56/stack/services/src/test/java/org/apache/usergrid/security/PasswordPolicyTest.java ---------------------------------------------------------------------- diff --git a/stack/services/src/test/java/org/apache/usergrid/security/PasswordPolicyTest.java b/stack/services/src/test/java/org/apache/usergrid/security/PasswordPolicyTest.java new file mode 100644 index 0000000..1599b18 --- /dev/null +++ b/stack/services/src/test/java/org/apache/usergrid/security/PasswordPolicyTest.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.usergrid.security; + +import org.apache.usergrid.ServiceITSetup; +import org.apache.usergrid.ServiceITSetupImpl; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + + +public class PasswordPolicyTest { + + @ClassRule + public static ServiceITSetup setup = new ServiceITSetupImpl(); + + + @Test + public void testBasicOperation() { + + PasswordPolicyImpl passwordPolicy = + (PasswordPolicyImpl)setup.getInjector().getInstance( PasswordPolicy.class ); + + Assert.assertEquals( 4, passwordPolicy.policyCheck( "secret", 12, 1, 1, 1 ).size() ); + Assert.assertEquals( 3, passwordPolicy.policyCheck( "Secret", 12, 1, 1, 1 ).size() ); + Assert.assertEquals( 2, passwordPolicy.policyCheck( "Secr3t", 12, 1, 1, 1 ).size() ); + Assert.assertEquals( 1, passwordPolicy.policyCheck( "Secr3t!", 12, 1, 1, 1 ).size() ); + Assert.assertEquals( 0, passwordPolicy.policyCheck( "Secr3t!longer", 12, 1, 1, 1 ).size() ); + + } + +} http://git-wip-us.apache.org/repos/asf/usergrid/blob/a30e1a56/stack/services/src/test/java/org/apache/usergrid/security/PasswordPolicyTestFig.java ---------------------------------------------------------------------- diff --git a/stack/services/src/test/java/org/apache/usergrid/security/PasswordPolicyTestFig.java b/stack/services/src/test/java/org/apache/usergrid/security/PasswordPolicyTestFig.java new file mode 100644 index 0000000..27a74d0 --- /dev/null +++ b/stack/services/src/test/java/org/apache/usergrid/security/PasswordPolicyTestFig.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.usergrid.security; + +import org.apache.usergrid.ServiceITSetup; +import org.apache.usergrid.ServiceITSetupImpl; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.safehaus.guicyfig.Bypass; +import org.safehaus.guicyfig.OptionState; +import org.safehaus.guicyfig.Overrides; + +import java.beans.PropertyChangeListener; +import java.util.Map; +import java.util.Properties; + + +public class PasswordPolicyTestFig implements PasswordPolicyFig { + + + @Override + public int getMinUppercaseAdmin() { + return 1; + } + + @Override + public int getMinUppercase() { + return 1; + } + + @Override + public int getMinDigitsAdmin() { + return 1; + } + + @Override + public int getMinDigits() { + return 1; + } + + @Override + public int getMinSpecialCharsAdmin() { + return 1; + } + + @Override + public int getMinSpecialChars() { + return 1; + } + + @Override + public int getMinLengthAdmin() { + return 1; + } + + @Override + public int getMinLength() { + return 1; + } + + @Override + public String getAllowedSpecialChars() { + return null; + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + + } + + @Override + public OptionState[] getOptions() { + return new OptionState[0]; + } + + @Override + public OptionState getOption(String key) { + return null; + } + + @Override + public String getKeyByMethod(String methodName) { + return null; + } + + @Override + public Object getValueByMethod(String methodName) { + return null; + } + + @Override + public Properties filterOptions(Properties properties) { + return null; + } + + @Override + public Map<String, Object> filterOptions(Map<String, Object> entries) { + return null; + } + + @Override + public void override(String key, String override) { + + } + + @Override + public boolean setOverrides(Overrides overrides) { + return false; + } + + @Override + public Overrides getOverrides() { + return null; + } + + @Override + public void bypass(String key, String bypass) { + + } + + @Override + public boolean setBypass(Bypass bypass) { + return false; + } + + @Override + public Bypass getBypass() { + return null; + } + + @Override + public Class getFigInterface() { + return null; + } + + @Override + public boolean isSingleton() { + return false; + } +}