Skip to content

Commit

Permalink
Merged in feature/SHIBUI-1030 (pull request #270)
Browse files Browse the repository at this point in the history
Feature/SHIBUI-1030
  • Loading branch information
Bill Smith authored and Jonathan Johnson committed Jan 17, 2019
2 parents f1a96cf + f3b48a9 commit 35a44a4
Show file tree
Hide file tree
Showing 17 changed files with 366 additions and 9 deletions.
21 changes: 19 additions & 2 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ plugins {
id 'io.franzbecker.gradle-lombok' version '1.13'
id 'com.palantir.docker' version '0.20.1'
id 'com.palantir.docker-run' version '0.20.1'
id 'com.avast.gradle.docker-compose' version '0.8.0'
}

apply plugin: 'io.spring.dependency-management'
Expand Down Expand Up @@ -110,7 +111,7 @@ dependencies {
}

// spring boot auto-config starters
['starter-web', 'starter-data-jpa', 'starter-security', 'starter-actuator', 'devtools', 'starter-webflux'].each {
['starter-web', 'starter-data-jpa', 'starter-security', 'starter-actuator', 'devtools', 'starter-webflux', 'starter-thymeleaf', 'starter-mail'].each {
compile "org.springframework.boot:spring-boot-${it}"
}
// TODO: figure out what this should really be
Expand Down Expand Up @@ -313,4 +314,20 @@ task runChecker << {
sleep 5000
}
}
}
}

/*
* Docker Compose (gradle-docker-compose-plugin) settings.
* Used to start and stop docker containers before running tests.
*/
apply plugin: 'docker-compose'
dockerCompose {
useComposeFiles = ['./src/test/docker-files/docker-compose.yml']
captureContainersOutput = true
waitForTcpPorts = false
}

test {
dependsOn 'composeUp'
finalizedBy 'composeDown'
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ReloadableMetadat
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.util.ModelRepresentationConversions
import org.springframework.context.annotation.Bean
Expand All @@ -24,39 +25,65 @@ import javax.annotation.PostConstruct
@Profile('dev')
class DevConfig {
private final UserRepository adminUserRepository
private final RoleRepository roleRepository

private final MetadataResolverRepository metadataResolverRepository

DevConfig(UserRepository adminUserRepository, MetadataResolverRepository metadataResolverRepository) {
DevConfig(UserRepository adminUserRepository, MetadataResolverRepository metadataResolverRepository, RoleRepository roleRepository) {
this.adminUserRepository = adminUserRepository
this.metadataResolverRepository = metadataResolverRepository
this.roleRepository = roleRepository
}

@Transactional
@PostConstruct
void createDevUsers() {
if (roleRepository.count() == 0) {
def roles = [new Role().with {
name = 'ROLE_ADMIN'
it
}, new Role().with {
name = 'ROLE_USER'
it
}, new Role().with {
name = 'ROLE_NONE'
it
}]
roles.each {
roleRepository.save(it)
}
}
roleRepository.flush()
if (adminUserRepository.count() == 0) {
def users = [new User().with {
username = 'admin'
password = '{noop}adminpass'
firstName = 'Joe'
lastName = 'Doe'
emailAddress = 'joe@institution.edu'
roles.add(new Role(name: 'ROLE_ADMIN'))
roles.add(roleRepository.findByName('ROLE_ADMIN').get())
it
}, new User().with {
username = 'nonadmin'
password = '{noop}nonadminpass'
firstName = 'Peter'
lastName = 'Vandelay'
emailAddress = 'peter@institution.edu'
roles.add(new Role(name: 'ROLE_USER'))
roles.add(roleRepository.findByName('ROLE_USER').get())
it
}, new User().with {
username = 'admin2'
password = '{noop}anotheradmin'
firstName = 'Rand'
lastName = 'al\'Thor'
emailAddress = 'rand@institution.edu'
roles.add(roleRepository.findByName('ROLE_ADMIN').get())
it
}]
users.each {
adminUserRepository.save(it)
}

adminUserRepository.flush()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class UserBootstrap {
new CSVReader(new InputStreamReader(shibUIConfiguration.userBootstrapResource.inputStream)).each { it ->
def (username, password, firstName, lastName, roleName) = it
def role = roleRepository.findByName(roleName).orElse(new Role(name: roleName))
roleRepository.saveAndFlush(role)
def user = userRepository.findByUsername(username).orElse(new User(username: username)).with {
it.password = password
it.firstName = firstName
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package edu.internet2.tier.shibboleth.admin.ui.configuration;

import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository;
import edu.internet2.tier.shibboleth.admin.ui.service.EmailService;
import edu.internet2.tier.shibboleth.admin.ui.service.EmailServiceImpl;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.mail.javamail.JavaMailSender;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;

import java.util.Collections;

/**
* @author Bill Smith (wsmith@unicon.net)
*/
@Configuration
@ConfigurationProperties("shibui.mail")
public class EmailConfiguration {

private static final String EMAIL_TEMPLATE_ENCODING = "UTF-8";

//Configured via @ConfigurationProperties (using setter method) with 'shibui.mail.text-email-template-path-prefix' property and
// default value set here if that property is not explicitly set in application.properties
@Setter
private String textEmailTemplatePathPrefix = "/mail/text/";

//Configured via @ConfigurationProperties (using setter method) with 'shibui.mail.html-email-template-path-prefix' property and
// default value set here if that property is not explicitly set in application.properties
@Setter
private String htmlEmailTemplatePathPrefix = "/mail/html/";

//Configured via @ConfigurationProperties (using setter method) with 'shibui.mail.system-email-address' property and
// default value set here if that property is not explicitly set in application.properties
@Setter
private String systemEmailAddress = "doNotReply@shibui.org";

@Autowired
private JavaMailSender javaMailSender;

@Autowired
private UserRepository userRepository;

@Bean
public ResourceBundleMessageSource emailMessageSource() {
final ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("mail/mailMessages");
return messageSource;
}

@Bean
public TemplateEngine textEmailTemplateEngine() {
final SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.addTemplateResolver(textTemplateResolver());
templateEngine.setTemplateEngineMessageSource(emailMessageSource());
return templateEngine;
}

@Bean
public TemplateEngine htmlEmailTemplateEngine() {
final SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.addTemplateResolver(htmlTemplateResolver());
templateEngine.setTemplateEngineMessageSource(emailMessageSource());
return templateEngine;
}

private ITemplateResolver textTemplateResolver() {
final ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
templateResolver.setOrder(1);
templateResolver.setResolvablePatterns(Collections.singleton("*"));
templateResolver.setPrefix(textEmailTemplatePathPrefix);
templateResolver.setSuffix(".txt");
templateResolver.setTemplateMode(TemplateMode.TEXT);
templateResolver.setCharacterEncoding(EMAIL_TEMPLATE_ENCODING);
templateResolver.setCacheable(false);
return templateResolver;
}

private ITemplateResolver htmlTemplateResolver() {
final ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
templateResolver.setOrder(1);
templateResolver.setResolvablePatterns(Collections.singleton("*"));
templateResolver.setPrefix(htmlEmailTemplatePathPrefix);
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setCharacterEncoding(EMAIL_TEMPLATE_ENCODING);
templateResolver.setCacheable(false);
return templateResolver;
}

@Bean
public EmailService emailService() {
return new EmailServiceImpl(javaMailSender,
emailMessageSource(),
textEmailTemplateEngine(),
htmlEmailTemplateEngine(),
systemEmailAddress,
userRepository);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration;
import edu.internet2.tier.shibboleth.admin.ui.security.model.Role;
import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository;
import edu.internet2.tier.shibboleth.admin.ui.service.EmailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public Role(String name, int rank) {

//Ignore properties annotation here is to prevent stack overflow recursive error during JSON serialization
@JsonIgnoreProperties("roles")
@ManyToMany(mappedBy = "roles", fetch = FetchType.EAGER)
@ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
private Set<User> users = new HashSet<>();

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
import lombok.ToString;
import org.apache.commons.lang.StringUtils;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
Expand Down Expand Up @@ -50,7 +50,7 @@ public class User extends AbstractAuditable {
private String role;

@JsonIgnore
@ManyToMany(cascade = CascadeType.PERSIST)
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;
import java.util.Set;

/**
* Spring Data repository to manage entities of type {@link User}.
Expand All @@ -13,4 +14,5 @@
public interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findByUsername(String username);
Set<User> findByRoles_Name(String roleName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package edu.internet2.tier.shibboleth.admin.ui.service;

import javax.mail.MessagingException;
import java.util.Locale;

/**
* @author Bill Smith (wsmith@unicon.net)
*/
public interface EmailService {
void sendMail(String emailTemplate, String fromAddress, String[] recipients, String subject, Locale locale) throws MessagingException;
void sendNewUserMail(String newUsername) throws MessagingException;
String[] getSystemAdminEmailAddresses();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package edu.internet2.tier.shibboleth.admin.ui.service;

import edu.internet2.tier.shibboleth.admin.ui.security.model.User;
import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;

/**
* @author Bill Smith (wsmith@unicon.net)
*/
public class EmailServiceImpl implements EmailService {
private static final Logger logger = LoggerFactory.getLogger(EmailServiceImpl.class);

private final String systemEmailAddress;
private JavaMailSender emailSender;
private ResourceBundleMessageSource emailMessageSource;
private TemplateEngine textEmailTemplateEngine;
private TemplateEngine htmlEmailTemplateEngine;
private UserRepository userRepository;

public EmailServiceImpl(JavaMailSender emailSender,
ResourceBundleMessageSource emailMessageSource,
TemplateEngine textEmailTemplateEngine,
TemplateEngine htmlEmailTemplateEngine,
String systemEmailAddress,
UserRepository userRepository) {
this.emailSender = emailSender;
this.emailMessageSource = emailMessageSource;
this.textEmailTemplateEngine = textEmailTemplateEngine;
this.htmlEmailTemplateEngine = htmlEmailTemplateEngine;
this.systemEmailAddress = systemEmailAddress;
this.userRepository = userRepository;
}

public void sendMail(String emailTemplate, String fromAddress, String[] recipients, String subject, Locale locale) throws MessagingException {
Context context = new Context(locale);
// TODO: set things to be replaced in the email template here

MimeMessage mimeMessage = this.emailSender.createMimeMessage();
MimeMessageHelper message = new MimeMessageHelper(mimeMessage, true,"UTF-8");
message.setSubject(subject);
message.setFrom(fromAddress);
message.setTo(recipients);

String textContent = textEmailTemplateEngine.process(emailTemplate, context);
String htmlContent = htmlEmailTemplateEngine.process(emailTemplate, context);
message.setText(textContent, htmlContent);

emailSender.send(mimeMessage);
}

public void sendNewUserMail(String newUsername) throws MessagingException {
String subject = String.format("User Access Request for %s", newUsername);
sendMail("new-user", systemEmailAddress, getSystemAdminEmailAddresses(), subject, Locale.getDefault());
}

public String[] getSystemAdminEmailAddresses() {
Set<User> systemAdmins = userRepository.findByRoles_Name("ROLE_ADMIN");
if (systemAdmins == null || systemAdmins.size() == 0) {
//TODO: Should this be an exception?
logger.warn("No users with ROLE_ADMIN were found! Check your configuration!");
systemAdmins = new HashSet<>();
}
return systemAdmins.stream().map(User::getEmailAddress).distinct().toArray(String[]::new);
}
}
12 changes: 12 additions & 0 deletions backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,15 @@ shibui.nameid-filter-ui-schema-location=classpath:nameid-filter.schema.json
# 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

# Email configuration (local mailhog)
spring.mail.host=localhost
spring.mail.port=1025
spring.mail.username=username
spring.mail.password=password
spring.mail.properties.mail.smtp.auth=false
spring.mail.properties.mail.smtp.starttls.enable=false

shibui.mail.text-email-template-path-prefix=/mail/text/
shibui.mail.html.email-template-path-prefix=/mail/html/
shibui.mail.system-email-address=doNotReply@shibui.org
4 changes: 4 additions & 0 deletions backend/src/main/resources/mail/html/new-user.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<html>
<head>New User Email</head>
<body>The user identified in the subject has requested access to SHIBUI.</body>
</html>
Loading

0 comments on commit 35a44a4

Please sign in to comment.