diff --git a/config/src/main/java/org/springframework/security/config/core/userdetails/UserDetailsResourceFactoryBean.java b/config/src/main/java/org/springframework/security/config/core/userdetails/UserDetailsResourceFactoryBean.java new file mode 100644 index 0000000000..67da735079 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/core/userdetails/UserDetailsResourceFactoryBean.java @@ -0,0 +1,132 @@ +/* + * + * * Copyright 2002-2017 the original author or authors. + * * + * * Licensed 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.springframework.security.config.core.userdetails; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.memory.UserAttribute; +import org.springframework.security.core.userdetails.memory.UserAttributeEditor; +import org.springframework.util.Assert; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Properties; + +/** + * Parses a Resource that is a Properties file in the format of: + * + * + * username=password[,enabled|disabled],roles... + * + * + * The enabled and disabled properties are optional with enabled being the default. For example: + * + * + * user=password,ROLE_USER + * admin=secret,ROLE_USER,ROLE_ADMIN + * disabled_user=does_not_matter,disabled,ROLE_USER + * + * + * @author Rob Winch + * @since 5.0 + */ +public class UserDetailsResourceFactoryBean implements ResourceLoaderAware, FactoryBean> { + private ResourceLoader resourceLoader; + + private String propertiesResourceLocation; + + private Resource propertiesResource; + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + @Override + public Collection getObject() throws Exception { + Properties userProperties = new Properties(); + Resource resource = getProperitesResource(); + try(InputStream in = resource.getInputStream()){ + userProperties.load(in); + } + + Collection users = new ArrayList<>(userProperties.size()); + Enumeration names = userProperties.propertyNames(); + UserAttributeEditor editor = new UserAttributeEditor(); + + while (names.hasMoreElements()) { + String name = (String) names.nextElement(); + String property = userProperties.getProperty(name); + editor.setAsText(property); + UserAttribute attr = (UserAttribute) editor.getValue(); + if(attr == null) { + throw new IllegalStateException("The entry with username '" + name + "' and value '" + property + "' could not be converted to a UserDetails."); + } + UserDetails user = User.withUsername(name) + .password(attr.getPassword()) + .disabled(!attr.isEnabled()) + .authorities(attr.getAuthorities()) + .build(); + users.add(user); + } + return users; + } + + @Override + public Class getObjectType() { + return Collection.class; + } + + /** + * Sets a the location of a Resource that is a Properties file in the format defined in {@link UserDetailsResourceFactoryBean} + * + * @param propertiesResourceLocation the location of the properties file that contains the users (i.e. "classpath:users.properties") + */ + public void setPropertiesResourceLocation(String propertiesResourceLocation) { + this.propertiesResourceLocation = propertiesResourceLocation; + } + + /** + * Sets a a Resource that is a Properties file in the format defined in {@link UserDetailsResourceFactoryBean} + * + * @param propertiesResource the Resource to use + */ + public void setPropertiesResource(Resource propertiesResource) { + this.propertiesResource = propertiesResource; + } + + private Resource getProperitesResource() { + if(propertiesResource != null) { + return propertiesResource; + } + if(propertiesResourceLocation != null) { + Assert.notNull(resourceLoader, "resourceLoader cannot be null if propertiesResource is null"); + return resourceLoader.getResource(propertiesResourceLocation); + } + throw new IllegalStateException("Either propertiesResource cannot be null or both resourceLoader and propertiesResourceLocation cannot be null"); + } +} diff --git a/config/src/test/java/org/springframework/security/config/core/userdetails/UserDetailsResourceFactoryBeanTest.java b/config/src/test/java/org/springframework/security/config/core/userdetails/UserDetailsResourceFactoryBeanTest.java new file mode 100644 index 0000000000..6df0ed1a4b --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/core/userdetails/UserDetailsResourceFactoryBeanTest.java @@ -0,0 +1,119 @@ +/* + * + * * Copyright 2002-2017 the original author or authors. + * * + * * Licensed 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.springframework.security.config.core.userdetails; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.util.InMemoryResource; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Collection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +/** + * @author Rob Winch + * @since 5.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class UserDetailsResourceFactoryBeanTest { + String location = "classpath:users.properties"; + + @Mock + ResourceLoader resourceLoader; + + UserDetailsResourceFactoryBean factory = new UserDetailsResourceFactoryBean(); + + @Test + public void getObjectWhenResourceLoaderNullThenThrowsIllegalStateException() throws Exception { + factory.setPropertiesResourceLocation(location); + + assertThatThrownBy(() -> factory.getObject() ) + .isInstanceOf(IllegalArgumentException.class) + .hasStackTraceContaining("resourceLoader cannot be null if propertiesResource is null"); + } + + @Test + public void getObjectWhenPropertiesResourceLocationNullThenThrowsIllegalStateException() throws Exception { + factory.setResourceLoader(resourceLoader); + + assertThatThrownBy(() -> factory.getObject() ) + .isInstanceOf(IllegalStateException.class) + .hasStackTraceContaining("Either propertiesResource cannot be null or both resourceLoader and propertiesResourceLocation cannot be null"); + } + + @Test + public void getObjectWhenPropertiesResourceLocationSingleUserThenThrowsGetsSingleUser() throws Exception { + setResource("user=password,ROLE_USER"); + + Collection users = factory.getObject(); + + UserDetails expectedUser = User.withUsername("user") + .password("password") + .authorities("ROLE_USER") + .build(); + assertThat(users).containsExactly(expectedUser); + } + + @Test + public void getObjectWhenPropertiesResourceSingleUserThenThrowsGetsSingleUser() throws Exception { + factory.setPropertiesResource(new InMemoryResource("user=password,ROLE_USER")); + + Collection users = factory.getObject(); + + UserDetails expectedUser = User.withUsername("user") + .password("password") + .authorities("ROLE_USER") + .build(); + assertThat(users).containsExactly(expectedUser); + } + + @Test + public void getObjectWhenInvalidUserThenThrowsMeaningfulException() throws Exception { + setResource("user=invalidFormatHere"); + + + assertThatThrownBy(() -> factory.getObject() ) + .isInstanceOf(IllegalStateException.class) + .hasStackTraceContaining("user") + .hasStackTraceContaining("invalidFormatHere"); + } + + private void setResource(String contents) throws IOException { + Resource resource = new InMemoryResource(contents); + when(resourceLoader.getResource(location)).thenReturn(resource); + + factory.setPropertiesResourceLocation(location); + factory.setResourceLoader(resourceLoader); + } +} diff --git a/config/src/test/resources/users.properties b/config/src/test/resources/users.properties new file mode 100644 index 0000000000..f1ea5d2cbf --- /dev/null +++ b/config/src/test/resources/users.properties @@ -0,0 +1,19 @@ +# +# /* +# * Copyright 2002-2017 the original author or authors. +# * +# * Licensed 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. +# */ +# + +user=password,ROLE_USER