diff --git a/backend/build.gradle b/backend/build.gradle index f03f9f9ed..3b6f64ddc 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -139,6 +139,9 @@ dependencies { //JSON schema validator compile 'org.sharegov:mjson:1.4.1' + + // CSV file support + compile 'com.opencsv:opencsv:4.4' } def generatedSrcDir = new File(buildDir, 'generated/src/main/java') diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy new file mode 100644 index 000000000..39fb575d8 --- /dev/null +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy @@ -0,0 +1,48 @@ +package edu.internet2.tier.shibboleth.admin.ui.service + +import com.opencsv.CSVReader +import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import groovy.util.logging.Slf4j +import org.springframework.boot.context.event.ApplicationStartedEvent +import org.springframework.context.event.EventListener +import org.springframework.stereotype.Component + +import javax.transaction.Transactional + +@Component +@Slf4j +class UserBootstrap { + private final ShibUIConfiguration shibUIConfiguration + private final UserRepository userRepository + private final RoleRepository roleRepository + + UserBootstrap(ShibUIConfiguration shibUIConfiguration, UserRepository userRepository, RoleRepository roleRepository) { + this.shibUIConfiguration = shibUIConfiguration + this.userRepository = userRepository + this.roleRepository = roleRepository + } + + @Transactional + @EventListener + void bootstrapUsersAndRoles(ApplicationStartedEvent e) { + if (shibUIConfiguration.userBootstrapResource.isPresent()) { + log.info("configuring users from ${shibUIConfiguration.userBootstrapResource.get().URI}") + new CSVReader(new InputStreamReader(shibUIConfiguration.userBootstrapResource.get().inputStream)).each { it -> + def (username, password, firstName, lastName, roleName) = it + def role = roleRepository.findByName(roleName).orElse(roleRepository.save(new Role(name: roleName))) + def user = userRepository.findByUsername(username).orElse(new User(username: username)).with { + it.password = password + it.firstName = firstName + it.lastName = lastName + it.roles.add(role) + it + } + userRepository.save(user) + } + } + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java index 38862308c..2bd90bcc3 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java @@ -1,14 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui; -import edu.internet2.tier.shibboleth.admin.ui.configuration.auto.WebSecurityConfig; -import edu.internet2.tier.shibboleth.admin.ui.configuration.auto.WebSecurityConfig.SupportedRoles; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; -import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; -import edu.internet2.tier.shibboleth.admin.ui.security.model.User; -import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository; -import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; -import edu.internet2.tier.shibboleth.admin.ui.service.UsersCsvParserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -25,16 +18,9 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - @SpringBootApplication @ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = "edu.internet2.tier.shibboleth.admin.ui.configuration.auto.*")) @EntityScan(basePackages = {"edu.internet2.tier.shibboleth.admin.ui.domain", "edu.internet2.tier.shibboleth.admin.ui.security.model"}) @@ -87,58 +73,4 @@ public void initializeResolvers(ApplicationStartedEvent e) { }); } } - - @Component - public static class UsersBootstrapStartupListener { - - @Autowired - UsersCsvParserService usersCsvParserService; - - @Autowired - UserRepository userRepository; - - @Autowired - RoleRepository roleRepository; - - private static final PasswordEncoder ENCODER = new BCryptPasswordEncoder(); - - @Transactional - @EventListener - public void bootstrapUsersAndRoles(ApplicationStartedEvent e) { - bootstrapRoles(); - List users = usersCsvParserService.parseUsersFromCsv(); - for (User user : users) { - User toBePersistedUser; - Optional existingUser = userRepository.findByUsername(user.getUsername()); - if (existingUser.isPresent()) { - toBePersistedUser = existingUser.get(); - } else { - toBePersistedUser = new User(); - toBePersistedUser.setUsername(user.getUsername()); - } - toBePersistedUser.setFirstName(user.getFirstName()); - toBePersistedUser.setLastName(user.getLastName()); - toBePersistedUser.setPassword(ENCODER.encode(user.getPassword())); - Set toBePersistedRoles = new HashSet<>(); - for (Role role : user.getRoles()) { - Optional existingRole = roleRepository.findByName(role.getName()); - if (existingRole.isPresent()) { - toBePersistedRoles.add(existingRole.get()); - } - } - toBePersistedUser.setRoles(toBePersistedRoles); - userRepository.save(toBePersistedUser); - } - } - - private void bootstrapRoles() { - for (SupportedRoles role : SupportedRoles.values()) { - if (!roleRepository.findByName(role.name()).isPresent()) { - Role toBePersistedRole = new Role(); - toBePersistedRole.setName(role.name()); - roleRepository.save(toBePersistedRole); - } - } - } - } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java index c49c3ee8c..fc1e0e8b4 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java @@ -23,7 +23,6 @@ import edu.internet2.tier.shibboleth.admin.ui.service.JPAMetadataResolverServiceImpl; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolversPositionOrderContainerService; -import edu.internet2.tier.shibboleth.admin.ui.service.UsersCsvParserService; import edu.internet2.tier.shibboleth.admin.util.AttributeUtility; import edu.internet2.tier.shibboleth.admin.util.LuceneUtility; import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; @@ -195,9 +194,4 @@ public Module stringTrimModule() { public ModelRepresentationConversions modelRepresentationConversions() { return new ModelRepresentationConversions(customPropertiesConfiguration()); } - - @Bean - public UsersCsvParserService usersCsvParserService() { - return new UsersCsvParserService(); - } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/ShibUIConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/ShibUIConfiguration.java new file mode 100644 index 000000000..a926a48b3 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/ShibUIConfiguration.java @@ -0,0 +1,32 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; + +import java.util.List; +import java.util.Optional; + +@Configuration +@ConfigurationProperties(prefix = "shibui") +@Getter +@Setter +public class ShibUIConfiguration { + /** + * A list of namespaces that should be excluded from incoming metadata. This is used to prevent third party metadata + * sources from using attributes that they might not have the rights to use. + */ + private List protectedAttributeNamespaces; + + /** + * A Resource containing a CSV of users to bootstrap into the system. Currently, this must be in format + * + * username,password,firstName,lastName,role + * + * Note that the password must be encrypted in the file using the system configured password encryption (by default, + * bcrypt) + */ + private Optional userBootstrapResource; +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java index 4f2368961..1dcdc6ce7 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java @@ -118,10 +118,5 @@ public void configure(WebSecurity web) throws Exception { } }; } - - public enum SupportedRoles { - ROLE_ADMIN, - ROLE_USER - } } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 71af41365..9ef60951e 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -64,9 +64,3 @@ shibui.entity-attributes-filters-ui-schema-location=classpath:entity-attributes- # Set the following property to periodically write out metadata providers configuration. There is no default value; the following is just an example # shibui.metadataProviders.target=file:/opt/shibboleth-idp/conf/shibui-metadata-providers.xml # shibui.metadataProviders.taskRunRate=30000 - -# CSV ile to bootstrap users from -# Entries should be of the form: -# username,password,firstName,lastName[,role1,role2,...,roleN} -# Note that the only roles currently supported are ROLE_ADMIN and ROLE_USER -bootstrap.users.csv.filename=bootstrap-users.csv \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy new file mode 100644 index 000000000..7798a2e39 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy @@ -0,0 +1,47 @@ +package edu.internet2.tier.shibboleth.admin.ui.service + +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.core.io.ClassPathResource +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration, ShibUIConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan(["edu.internet2.tier.shibboleth.admin.ui", "edu.internet2.tier.shibboleth.admin.ui.security.model"]) +@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) +class UserBootstrapTests extends Specification { + @Autowired + ShibUIConfiguration shibUIConfiguration + + @Autowired + UserRepository userRepository + + @Autowired + RoleRepository roleRepository + + def "simple test"() { + setup: + shibUIConfiguration.userBootstrapResource = Optional.of(new ClassPathResource('/conf/1044.csv')) + def userBootstrap = new UserBootstrap(shibUIConfiguration, userRepository, roleRepository) + + when: + userBootstrap.bootstrapUsersAndRoles(null) + + then: + noExceptionThrown() + assert userRepository.findAll().size() == 2 + assert roleRepository.findAll().size() == 2 + } +} diff --git a/backend/src/test/resources/conf/1044.csv b/backend/src/test/resources/conf/1044.csv new file mode 100644 index 000000000..666681a0b --- /dev/null +++ b/backend/src/test/resources/conf/1044.csv @@ -0,0 +1,2 @@ +"user1","password1","firstName1","lastName1","ROLE_ADMIN" +"user2","password2","firstName2","lastName2","ROLE_USER" \ No newline at end of file