From 5bfa4a80d37ffb518020bbfff6d36629ebce35aa Mon Sep 17 00:00:00 2001 From: Bill Smith Date: Tue, 11 Dec 2018 17:43:07 -0700 Subject: [PATCH] [SHIBUI-1044] First pass at adding support for bootstrapped users and roles. Roles are hardcoded and bootstrapped if necessary. Users are bootstrapped always, provided a users bootstrap csv is specified. --- .../ui/service/UsersCsvParserService.groovy | 59 ++++++++++++++++ .../admin/ui/ShibbolethUiApplication.java | 68 +++++++++++++++++++ .../CoreShibUiConfiguration.java | 6 ++ .../configuration/auto/WebSecurityConfig.java | 5 ++ .../src/main/resources/application.properties | 6 ++ 5 files changed, 144 insertions(+) create mode 100644 backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UsersCsvParserService.groovy diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UsersCsvParserService.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UsersCsvParserService.groovy new file mode 100644 index 000000000..d83692f50 --- /dev/null +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UsersCsvParserService.groovy @@ -0,0 +1,59 @@ +package edu.internet2.tier.shibboleth.admin.ui.service + +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import liquibase.util.StringUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +class UsersCsvParserService { + + private static final Logger logger = LoggerFactory.getLogger(UsersCsvParserService.class) + + //TODO: Is there a better, springy way to do this? + String getUsersCsvFilename() { + Properties properties = new Properties() + properties.load(getClass().classLoader.getResourceAsStream('application.properties')) + properties.get('bootstrap.users.csv.filename') + } + + List parseUsersFromCsv() { + def usersFilename = getUsersCsvFilename() + def users = null + if (StringUtils.isNotEmpty(usersFilename)) { + InputStream inputFile = getClass().classLoader.getResourceAsStream(usersFilename) + if (inputFile != null) { + List rows = inputFile.text.split('\n').collect { it.split(',') } + + rows.findAll { it.size() < 4 }.each { + logger.warn('Invalid entry detected in {} -> {}', usersFilename, it) + logger.warn('Entries are of the form: username,password,firstName,lastName[,role1,role2,...,roleN]') + } + users = rows.findAll { it.size() > 3 }.collect { row -> + new User().with { + username = row[0] + password = row[1] + firstName = row[2] + lastName = row[3] + roles = new HashSet() + (4..row.size() - 1).each { roleIndex -> + roles.add(new Role().with { + name = row[roleIndex] + it + }) + } + it + } + } + } else { + logger.error('The application.properties property [bootstrap.users.csv.filename] specifies a file [{}] that was not found.', usersFilename) + } + } else { + logger.info('No bootstrap.users.csv.filename specified in application.properties.') + } + users ?: new ArrayList() + } +} \ No newline at end of file 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 2bd90bcc3..38862308c 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,7 +1,14 @@ 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; @@ -18,9 +25,16 @@ 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"}) @@ -73,4 +87,58 @@ 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 fc1e0e8b4..c49c3ee8c 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,6 +23,7 @@ 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; @@ -194,4 +195,9 @@ 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/auto/WebSecurityConfig.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java index 1dcdc6ce7..4f2368961 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,5 +118,10 @@ 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 9ef60951e..71af41365 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -64,3 +64,9 @@ 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