Skip to content

Commit

Permalink
[SHIBUI-1044]
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Bill Smith committed Dec 12, 2018
1 parent 3d36ed9 commit 5bfa4a8
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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<User> parseUsersFromCsv() {
def usersFilename = getUsersCsvFilename()
def users = null
if (StringUtils.isNotEmpty(usersFilename)) {
InputStream inputFile = getClass().classLoader.getResourceAsStream(usersFilename)
if (inputFile != null) {
List<String[]> 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<Role>()
(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<User>()
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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"})
Expand Down Expand Up @@ -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<User> users = usersCsvParserService.parseUsersFromCsv();
for (User user : users) {
User toBePersistedUser;
Optional<User> 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<Role> toBePersistedRoles = new HashSet<>();
for (Role role : user.getRoles()) {
Optional<Role> 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);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -194,4 +195,9 @@ public Module stringTrimModule() {
public ModelRepresentationConversions modelRepresentationConversions() {
return new ModelRepresentationConversions(customPropertiesConfiguration());
}

@Bean
public UsersCsvParserService usersCsvParserService() {
return new UsersCsvParserService();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,10 @@ public void configure(WebSecurity web) throws Exception {
}
};
}

public enum SupportedRoles {
ROLE_ADMIN,
ROLE_USER
}
}

6 changes: 6 additions & 0 deletions backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 5bfa4a8

Please sign in to comment.