Skip to content

Commit

Permalink
Merged in SHIBUI-975 (pull request #234)
Browse files Browse the repository at this point in the history
SHIBUI-975
  • Loading branch information
dima767 authored and Jonathan Johnson committed Nov 19, 2018
2 parents f79efd5 + 94a6e62 commit a2cacf5
Show file tree
Hide file tree
Showing 13 changed files with 279 additions and 10 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ The easiest way to do this in a servlet container is through the use of system p

## Authentication

Currently, the application is wired with very simple authentication. A password for the user `user`
Currently, the application is wired with very simple authentication. A password for the user `root`
can be set with the `shibui.default-password` property. If none is set, a default password
will be generated and logged:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package edu.internet2.tier.shibboleth.admin.ui.configuration

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.UserRepository
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

import javax.annotation.PostConstruct

@Component
@Profile('dev')
class DevConfig {
private final UserRepository adminUserRepository

DevConfig(UserRepository adminUserRepository) {
this.adminUserRepository = adminUserRepository
}

@Transactional
@PostConstruct
void createDevAdminUsers() {
if (adminUserRepository.count() == 0) {
def user = new User().with {
username = 'admin'
password = '{noop}adminpass'
roles.add(new Role(name: 'ROLE_ADMIN'))
it
}

adminUserRepository.save(user)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@
import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Profile;
import org.springframework.context.event.EventListener;
Expand All @@ -21,7 +19,7 @@

@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")
@EntityScan(basePackages = {"edu.internet2.tier.shibboleth.admin.ui.domain", "edu.internet2.tier.shibboleth.admin.ui.security.model"})
@EnableJpaAuditing
@EnableScheduling
@EnableWebSecurity
Expand Down Expand Up @@ -49,5 +47,4 @@ void showMetadataResolversResourceIds(ApplicationStartedEvent e) {
.forEach(it -> System.out.println(String.format("MetadataResolver [%s: %s]", it.getName(), it.getResourceId())));
}
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package edu.internet2.tier.shibboleth.admin.ui.configuration.auto;

import edu.internet2.tier.shibboleth.admin.ui.security.DefaultAuditorAware;
import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository;
import edu.internet2.tier.shibboleth.admin.ui.security.springsecurity.AdminUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
Expand All @@ -13,14 +16,16 @@
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

/**
* Web security configuration.
*
* <p>
* Workaround for slashes in URL from [https://stackoverflow.com/questions/48453980/spring-5-0-3-requestrejectedexception-the-request-was-rejected-because-the-url]
*/
@Configuration
Expand All @@ -34,6 +39,9 @@ public class WebSecurityConfig {
@Value("${shibui.default-password:}")
private String defaultPassword;

@Autowired
private UserRepository userRepository;

@Bean
public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
Expand Down Expand Up @@ -62,13 +70,15 @@ protected void configure(HttpSecurity http) throws Exception {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// TODO: more configurable authentication
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
if (defaultPassword != null && !"".equals(defaultPassword)) {
auth
.inMemoryAuthentication()
.withUser("user").password(defaultPassword).roles("USER");
} else {
super.configure(auth);
.withUser("root")
.password(defaultPassword)
.roles("ADMIN");
}
auth.userDetailsService(adminUserService(userRepository)).passwordEncoder(passwordEncoder);
}

@Override
Expand All @@ -85,6 +95,12 @@ public AuditorAware<String> defaultAuditorAware() {
return new DefaultAuditorAware();
}

@Bean
@Profile("!no-auth")
public AdminUserService adminUserService(UserRepository userRepository) {
return new AdminUserService(userRepository);
}

@Bean
@Profile("no-auth")
public WebSecurityConfigurerAdapter noAuthUsedForEaseDevelopment() {
Expand All @@ -103,3 +119,4 @@ public void configure(WebSecurity web) throws Exception {
};
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package edu.internet2.tier.shibboleth.admin.ui.security.model;

import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.ManyToMany;
import java.util.HashSet;
import java.util.Set;

/**
* Models a basic administrative role concept in the system.
*
* @author Dmitriy Kopylenko
*/
@Entity
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode(callSuper = true, exclude = "users")
@ToString(exclude = "users")
public class Role extends AbstractAuditable {

@Column(unique = true)
private String name;

@ManyToMany(cascade = CascadeType.ALL, mappedBy = "roles", fetch = FetchType.EAGER)
private Set<User> users = new HashSet<>();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package edu.internet2.tier.shibboleth.admin.ui.security.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable;
import lombok.*;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import java.util.HashSet;
import java.util.Set;

/**
* Models a basic administrative user in the system.
*
* @author Dmitriy Kopylenko
*/
@Entity
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode(callSuper = true, exclude = "roles")
@ToString(exclude = "roles")
public class User extends AbstractAuditable {

@Column(nullable = false, unique = true)
private String username;

@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Column(nullable = false)
private String password;

private String firstName;

private String lastName;

@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package edu.internet2.tier.shibboleth.admin.ui.security.repository;

import edu.internet2.tier.shibboleth.admin.ui.security.model.Role;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

/**
* Spring Data repository to manage entities of type {@link Role}.
*
* @author Dmitriy Kopylenko
*/
public interface RoleRepository extends JpaRepository<Role, Long> {

Optional<Role> findByName(final String name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package edu.internet2.tier.shibboleth.admin.ui.security.repository;

import edu.internet2.tier.shibboleth.admin.ui.security.model.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

/**
* Spring Data repository to manage entities of type {@link User}.
*
* @author Dmitriy Kopylenko
*/
public interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findByUsername(String username);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package edu.internet2.tier.shibboleth.admin.ui.security.springsecurity;

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.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.transaction.annotation.Transactional;

import java.util.Set;
import static java.util.stream.Collectors.toSet;

/**
* Spring Security {@link UserDetailsService} implementation for local administration of admin users in the system.
*
* @author Dmitriy Kopylenko
*/
@RequiredArgsConstructor
public class AdminUserService implements UserDetailsService {

private final UserRepository userRepository;

@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository
.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(String.format("User [%s] is not found", username)));

Set<GrantedAuthority> grantedAuthorities = user.getRoles().stream()
.map(Role::getName)
.map(SimpleGrantedAuthority::new)
.collect(toSet());

if (grantedAuthorities.isEmpty()) {
//As defined by the UserDetailsService API contract
throw new UsernameNotFoundException(String.format("No roles are defined for user [%s]", username));
}

return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities);
}
}

1 change: 1 addition & 0 deletions backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Logging Configuration
#logging.config=classpath:log4j2.xml

#logging.level.org.springframework.security=INFO
logging.level.org.springframework=INFO
logging.level.edu.internet2.tier.shibboleth.admin.ui=INFO

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package edu.internet2.tier.shibboleth.admin.ui.security.springsecurity

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.test.context.SpringBootTest
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.test.context.ActiveProfiles
import spock.lang.Specification

/**
* Tests for <code>AdminUserService</code>
*
* @author Dmitriy Kopylenko
*/
@SpringBootTest
@ActiveProfiles('dev')
class AdminUserServiceTests extends Specification {

@Autowired
AdminUserService adminUserService

@Autowired
RoleRepository adminRoleRepository

@Autowired
UserRepository adminUserRepository


def "Loading existing admin user with admin role"() {
given: 'Valid user with admin role is available (loaded by Spring Boot Listener in dev profile)'
def user = adminUserService.loadUserByUsername('admin')

expect:
user.username == 'admin'
user.password == '{noop}adminpass'
user.getAuthorities().size() == 1
user.getAuthorities()[0].authority == 'ROLE_ADMIN'
user.enabled
user.accountNonExpired
user.credentialsNonExpired
}

def "Loading NON-existing admin user with admin role"() {
when: 'Non-existent admin user is tried to be looked up'
adminUserService.loadUserByUsername('nonexisting')

then:
thrown UsernameNotFoundException
}
}
2 changes: 1 addition & 1 deletion docs/GETTINGSTARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ The easiest way to do this in a servlet container is through the use of system p

## Authentication

Currently, the application is wired with very simple authentication. A password for the user `user`
Currently, the application is wired with very simple authentication. A password for the user `root`
can be set with the `shibui.default-password` property. If none is set, a default password
will be generated and logged:

Expand Down
8 changes: 8 additions & 0 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Security

Security in the system is controlled by Spring Security.

Currently, the following roles are recognized:

1. `ADMIN`
1. `USER`

0 comments on commit a2cacf5

Please sign in to comment.