diff --git a/pom.xml b/pom.xml index df7a6cb36..325766c77 100644 --- a/pom.xml +++ b/pom.xml @@ -138,11 +138,19 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-security + org.springframework.security spring-security-test test + + org.springframework.security + spring-security-data + diff --git a/src/main/java/guru/sfg/brewery/bootstrap/DefaultBreweryLoader.java b/src/main/java/guru/sfg/brewery/bootstrap/DefaultBreweryLoader.java index c8f04894b..6bda19322 100644 --- a/src/main/java/guru/sfg/brewery/bootstrap/DefaultBreweryLoader.java +++ b/src/main/java/guru/sfg/brewery/bootstrap/DefaultBreweryLoader.java @@ -17,12 +17,22 @@ package guru.sfg.brewery.bootstrap; import guru.sfg.brewery.domain.*; +import guru.sfg.brewery.domain.security.Authority; +import guru.sfg.brewery.domain.security.Role; +import guru.sfg.brewery.domain.security.User; import guru.sfg.brewery.repositories.*; +import guru.sfg.brewery.repositories.security.AuthorityRepository; +import guru.sfg.brewery.repositories.security.RoleRepository; +import guru.sfg.brewery.repositories.security.UserRepository; import guru.sfg.brewery.web.model.BeerStyleEnum; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.CommandLineRunner; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; +import java.util.Arrays; +import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -30,11 +40,19 @@ /** * Created by jt on 2019-01-26. */ +@Slf4j @RequiredArgsConstructor @Component public class DefaultBreweryLoader implements CommandLineRunner { public static final String TASTING_ROOM = "Tasting Room"; + public static final String ST_PETE_DISTRIBUTING = "St Pete Distributing"; + public static final String DUNEDIN_DISTRIBUTING = "Dunedin Distributing"; + public static final String KEY_WEST_DISTRIBUTORS = "Key West Distributors"; + public static final String STPETE_USER = "stpete"; + public static final String DUNEDIN_USER = "dunedin"; + public static final String KEYWEST_USER = "keywest"; + public static final String BEER_1_UPC = "0631234200036"; public static final String BEER_2_UPC = "0631234300019"; public static final String BEER_3_UPC = "0083783375213"; @@ -44,14 +62,75 @@ public class DefaultBreweryLoader implements CommandLineRunner { private final BeerInventoryRepository beerInventoryRepository; private final BeerOrderRepository beerOrderRepository; private final CustomerRepository customerRepository; + private final AuthorityRepository authorityRepository; + private final RoleRepository roleRepository; + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; @Override public void run(String... args) { + loadSecurityData(); loadBreweryData(); + loadTastingRoomData(); loadCustomerData(); } private void loadCustomerData() { + Role customerRole = roleRepository.findByName("CUSTOMER").orElseThrow(); + + //create customers + Customer stPeteCustomer = customerRepository.save(Customer.builder() + .customerName(ST_PETE_DISTRIBUTING) + .apiKey(UUID.randomUUID()) + .build()); + + Customer dunedinCustomer = customerRepository.save(Customer.builder() + .customerName(DUNEDIN_DISTRIBUTING) + .apiKey(UUID.randomUUID()) + .build()); + + Customer keyWestCustomer = customerRepository.save(Customer.builder() + .customerName(KEY_WEST_DISTRIBUTORS) + .apiKey(UUID.randomUUID()) + .build()); + + //create users + User stPeteUser = userRepository.save(User.builder().username(STPETE_USER) + .password(passwordEncoder.encode("password")) + .customer(stPeteCustomer) + .role(customerRole).build()); + + User dunedinUser = userRepository.save(User.builder().username(DUNEDIN_USER) + .password(passwordEncoder.encode("password")) + .customer(dunedinCustomer) + .role(customerRole).build()); + + User keywest = userRepository.save(User.builder().username(KEYWEST_USER) + .password(passwordEncoder.encode("password")) + .customer(keyWestCustomer) + .role(customerRole).build()); + + //create orders + createOrder(stPeteCustomer); + createOrder(dunedinCustomer); + createOrder(keyWestCustomer); + + log.debug("Orders Loaded: " + beerOrderRepository.count()); + } + + private BeerOrder createOrder(Customer customer) { + return beerOrderRepository.save(BeerOrder.builder() + .customer(customer) + .orderStatus(OrderStatusEnum.NEW) + .beerOrderLines(Set.of(BeerOrderLine.builder() + .beer(beerRepository.findByUpc(BEER_1_UPC)) + .orderQuantity(2) + .build())) + .build()); + } + + + private void loadTastingRoomData() { Customer tastingRoom = Customer.builder() .customerName(TASTING_ROOM) .apiKey(UUID.randomUUID()) @@ -122,4 +201,72 @@ private void loadBreweryData() { } } + + private void loadSecurityData() { + //beer auths + Authority createBeer = authorityRepository.save(Authority.builder().permission("beer.create").build()); + Authority readBeer = authorityRepository.save(Authority.builder().permission("beer.read").build()); + Authority updateBeer = authorityRepository.save(Authority.builder().permission("beer.update").build()); + Authority deleteBeer = authorityRepository.save(Authority.builder().permission("beer.delete").build()); + + //customer auths + Authority createCustomer = authorityRepository.save(Authority.builder().permission("customer.create").build()); + Authority readCustomer = authorityRepository.save(Authority.builder().permission("customer.read").build()); + Authority updateCustomer = authorityRepository.save(Authority.builder().permission("customer.update").build()); + Authority deleteCustomer = authorityRepository.save(Authority.builder().permission("customer.delete").build()); + + //customer brewery + Authority createBrewery = authorityRepository.save(Authority.builder().permission("brewery.create").build()); + Authority readBrewery = authorityRepository.save(Authority.builder().permission("brewery.read").build()); + Authority updateBrewery = authorityRepository.save(Authority.builder().permission("brewery.update").build()); + Authority deleteBrewery = authorityRepository.save(Authority.builder().permission("brewery.delete").build()); + + //beer order + Authority createOrder = authorityRepository.save(Authority.builder().permission("order.create").build()); + Authority readOrder = authorityRepository.save(Authority.builder().permission("order.read").build()); + Authority updateOrder = authorityRepository.save(Authority.builder().permission("order.update").build()); + Authority deleteOrder = authorityRepository.save(Authority.builder().permission("order.delete").build()); + Authority pickupOrder = authorityRepository.save(Authority.builder().permission("order.pickup").build()); + Authority createOrderCustomer = authorityRepository.save(Authority.builder().permission("customer.order.create").build()); + Authority readOrderCustomer = authorityRepository.save(Authority.builder().permission("customer.order.read").build()); + Authority updateOrderCustomer = authorityRepository.save(Authority.builder().permission("customer.order.update").build()); + Authority deleteOrderCustomer = authorityRepository.save(Authority.builder().permission("customer.order.delete").build()); + Authority pickupOrderCustomer = authorityRepository.save(Authority.builder().permission("customer.order.pickup").build()); + + Role adminRole = roleRepository.save(Role.builder().name("ADMIN").build()); + Role customerRole = roleRepository.save(Role.builder().name("CUSTOMER").build()); + Role userRole = roleRepository.save(Role.builder().name("USER").build()); + + adminRole.setAuthorities(new HashSet<>(Set.of(createBeer, updateBeer, readBeer, deleteBeer, createCustomer, readCustomer, + updateCustomer, deleteCustomer, createBrewery, readBrewery, updateBrewery, deleteBrewery, + createOrder, readOrder, updateOrder, deleteOrder, pickupOrder))); + + customerRole.setAuthorities(new HashSet<>(Set.of(readBeer, readCustomer, readBrewery, createOrderCustomer, readOrderCustomer, + updateOrderCustomer, deleteOrderCustomer, pickupOrderCustomer))); + + userRole.setAuthorities(new HashSet<>(Set.of(readBeer))); + + roleRepository.saveAll(Arrays.asList(adminRole, customerRole, userRole)); + + userRepository.save(User.builder() + .username("spring") + .password(passwordEncoder.encode("guru")) + .role(adminRole) + .build()); + + userRepository.save(User.builder() + .username("user") + .password(passwordEncoder.encode("password")) + .role(userRole) + .build()); + + userRepository.save(User.builder() + .username("scott") + .password(passwordEncoder.encode("tiger")) + .role(customerRole) + .build()); + + log.debug("Users Loaded: " + userRepository.count()); + } + } diff --git a/src/main/java/guru/sfg/brewery/config/BeerOrderAuthenticationManger.java b/src/main/java/guru/sfg/brewery/config/BeerOrderAuthenticationManger.java new file mode 100644 index 000000000..3315e8d2e --- /dev/null +++ b/src/main/java/guru/sfg/brewery/config/BeerOrderAuthenticationManger.java @@ -0,0 +1,19 @@ +package guru.sfg.brewery.config; + +import guru.sfg.brewery.domain.security.User; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Slf4j +@Component +public class BeerOrderAuthenticationManger { + + public boolean customerIdMatches(Authentication authentication, UUID customerId) { + User authenticatedUser = (User) authentication.getPrincipal(); + log.debug("Auth User Customer ID" + authenticatedUser.getCustomer().getId() + " Customer Id: " + customerId); + return authenticatedUser.getCustomer().getId().equals(customerId); + } +} diff --git a/src/main/java/guru/sfg/brewery/config/RestHeaderAuthFilter.java b/src/main/java/guru/sfg/brewery/config/RestHeaderAuthFilter.java new file mode 100644 index 000000000..5194dee0e --- /dev/null +++ b/src/main/java/guru/sfg/brewery/config/RestHeaderAuthFilter.java @@ -0,0 +1,120 @@ +package guru.sfg.brewery.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.StringUtils; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Created by jt on 6/19/20. + */ +@Slf4j +public class RestHeaderAuthFilter extends AbstractAuthenticationProcessingFilter { + + public RestHeaderAuthFilter(RequestMatcher requiresAuthenticationRequestMatcher) { + super(requiresAuthenticationRequestMatcher); + } + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + + + if (logger.isDebugEnabled()) { + logger.debug("Request is to process authentication"); + } + + Authentication authResult = null; + try { + authResult = attemptAuthentication(request, response); + if (authResult != null) { + successfulAuthentication(request, response, chain, authResult); + } else { + chain.doFilter(request, response); + } + } catch (AuthenticationException exception) { + log.error("Authentication failed" + exception); + unsuccessfulAuthentication(request, response, exception); + } + + + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { + String userName = getUsername(request); + String password = getPassword(request); + + if (userName == null) { + userName = ""; + } + + if (password == null) { + password = ""; + } + + log.debug("Authenticating User: " + userName); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName, password); + + if (!StringUtils.isEmpty(userName)) { + return this.getAuthenticationManager().authenticate(token); + } else { + return null; + } + } + + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, + HttpServletResponse response, AuthenticationException failed) + throws IOException, ServletException { + + SecurityContextHolder.clearContext(); + + if (log.isDebugEnabled()) { + log.debug("Authentication request failed: " + failed.toString(), failed); + log.debug("Updated SecurityContextHolder to contain null Authentication"); + } + + response.sendError(HttpStatus.UNAUTHORIZED.value(), + HttpStatus.UNAUTHORIZED.getReasonPhrase()); + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, + HttpServletResponse response, FilterChain chain, Authentication authResult) + throws IOException, ServletException { + + if (logger.isDebugEnabled()) { + logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + + authResult); + } + + SecurityContextHolder.getContext().setAuthentication(authResult); + + } + + private String getPassword(HttpServletRequest request) { + return request.getHeader("Api-Secret"); + } + + private String getUsername(HttpServletRequest request) { + return request.getHeader("Api-Key"); + } +} \ No newline at end of file diff --git a/src/main/java/guru/sfg/brewery/config/SecurityBeans.java b/src/main/java/guru/sfg/brewery/config/SecurityBeans.java new file mode 100644 index 000000000..8d662a50b --- /dev/null +++ b/src/main/java/guru/sfg/brewery/config/SecurityBeans.java @@ -0,0 +1,31 @@ +package guru.sfg.brewery.config; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationEventPublisher; +import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; +import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl; +import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; + +import javax.sql.DataSource; + +/** + * Created by jt on 7/17/20. + */ +@Configuration +public class SecurityBeans { + + @Bean + public PersistentTokenRepository persistentTokenRepository(DataSource dataSource) { + JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); + tokenRepository.setDataSource(dataSource); + return tokenRepository; + } + + @Bean + public AuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + return new DefaultAuthenticationEventPublisher(applicationEventPublisher); + } + +} \ No newline at end of file diff --git a/src/main/java/guru/sfg/brewery/config/SecurityConfig.java b/src/main/java/guru/sfg/brewery/config/SecurityConfig.java new file mode 100644 index 000000000..3680ca160 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/config/SecurityConfig.java @@ -0,0 +1,115 @@ +package guru.sfg.brewery.config; + + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension; +import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +/** + * Created by jt on 6/13/20. + */ +@RequiredArgsConstructor +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + private final UserDetailsService userDetailsService; + private final PersistentTokenRepository persistentTokenRepository; + + // needed for use with Spring Data JPA SPeL + @Bean + public SecurityEvaluationContextExtension securityEvaluationContextExtension() { + return new SecurityEvaluationContextExtension(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + + http + .authorizeRequests(authorize -> { + authorize + .antMatchers("/h2-console/**").permitAll() //do not use in production! + .antMatchers("/", "/webjars/**", "/login", "/resources/**").permitAll(); + } ) + .authorizeRequests() + .anyRequest().authenticated() + .and() + .formLogin(loginConfigurer -> { + loginConfigurer + .loginProcessingUrl("/login") + .loginPage("/").permitAll() + .successForwardUrl("/") + .defaultSuccessUrl("/") + .failureUrl("/?error"); + }) + .logout(logoutConfigurer -> { + logoutConfigurer + .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET")) + .logoutSuccessUrl("/?logout") + .permitAll(); + }) + .httpBasic() + .and().csrf().ignoringAntMatchers("/h2-console/**", "/api/**") + .and().rememberMe() + .tokenRepository(persistentTokenRepository) + .userDetailsService(userDetailsService); + + //.rememberMe() + //.key("sfg-key") + //.userDetailsService(userDetailsService); + + //h2 console config + http.headers().frameOptions().sameOrigin(); + } + + @Bean + PasswordEncoder passwordEncoder(){ + return SfgPasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + + // @Override + // protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // auth.userDetailsService(this.jpaUserDetailsService).passwordEncoder(passwordEncoder()); + +// auth.inMemoryAuthentication() +// .withUser("spring") +// .password("{bcrypt}$2a$10$7tYAvVL2/KwcQTcQywHIleKueg4ZK7y7d44hKyngjTwHCDlesxdla") +// .roles("ADMIN") +// .and() +// .withUser("user") +// .password("{sha256}1296cefceb47413d3fb91ac7586a4625c33937b4d3109f5a4dd96c79c46193a029db713b96006ded") +// .roles("USER"); +// +// auth.inMemoryAuthentication().withUser("scott").password("{bcrypt15}$2a$15$baOmQtw8UqWZRDQhMFPFj.xhkkWveCTQHe4OBdr8yw8QshejiSbI6").roles("CUSTOMER"); + // } + + // @Override +// @Bean +// protected UserDetailsService userDetailsService() { +// UserDetails admin = User.withDefaultPasswordEncoder() +// .username("spring") +// .password("guru") +// .roles("ADMIN") +// .build(); +// +// UserDetails user = User.withDefaultPasswordEncoder() +// .username("user") +// .password("password") +// .roles("USER") +// .build(); +// +// return new InMemoryUserDetailsManager(admin, user); +// } + + +} \ No newline at end of file diff --git a/src/main/java/guru/sfg/brewery/config/SfgPasswordEncoderFactories.java b/src/main/java/guru/sfg/brewery/config/SfgPasswordEncoderFactories.java new file mode 100644 index 000000000..5e47fb67a --- /dev/null +++ b/src/main/java/guru/sfg/brewery/config/SfgPasswordEncoderFactories.java @@ -0,0 +1,30 @@ +package guru.sfg.brewery.config; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.DelegatingPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.HashMap; +import java.util.Map; + +/** + * Created by jt on 6/17/20. + */ +public class SfgPasswordEncoderFactories { + + public static PasswordEncoder createDelegatingPasswordEncoder() { + String encodingId = "bcrypt10"; + Map encoders = new HashMap<>(); + encoders.put(encodingId, new BCryptPasswordEncoder(10)); + encoders.put("bcrypt", new BCryptPasswordEncoder()); + encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder()); + encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance()); + encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder()); + + return new DelegatingPasswordEncoder(encodingId, encoders); + } + + //don't instantiate class + private SfgPasswordEncoderFactories() { + } +} \ No newline at end of file diff --git a/src/main/java/guru/sfg/brewery/config/TaskConfig.java b/src/main/java/guru/sfg/brewery/config/TaskConfig.java new file mode 100644 index 000000000..92ece6b5e --- /dev/null +++ b/src/main/java/guru/sfg/brewery/config/TaskConfig.java @@ -0,0 +1,21 @@ +package guru.sfg.brewery.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * Task Configuration - enable asyc tasks + */ +@EnableScheduling +@EnableAsync +@Configuration +public class TaskConfig { + @Bean + TaskExecutor taskExecutor() { + return new SimpleAsyncTaskExecutor(); + } +} \ No newline at end of file diff --git a/src/main/java/guru/sfg/brewery/domain/Customer.java b/src/main/java/guru/sfg/brewery/domain/Customer.java index bd3c7a40f..b8c314c02 100644 --- a/src/main/java/guru/sfg/brewery/domain/Customer.java +++ b/src/main/java/guru/sfg/brewery/domain/Customer.java @@ -16,14 +16,13 @@ */ package guru.sfg.brewery.domain; +import guru.sfg.brewery.domain.security.User; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.OneToMany; +import javax.persistence.*; import java.sql.Timestamp; import java.util.Set; import java.util.UUID; @@ -54,4 +53,7 @@ public Customer(UUID id, Long version, Timestamp createdDate, Timestamp lastModi @OneToMany(mappedBy = "customer") private Set beerOrders; + @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, fetch = FetchType.EAGER) + private Set users; + } diff --git a/src/main/java/guru/sfg/brewery/domain/security/Authority.java b/src/main/java/guru/sfg/brewery/domain/security/Authority.java new file mode 100644 index 000000000..7209abee8 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/domain/security/Authority.java @@ -0,0 +1,27 @@ +package guru.sfg.brewery.domain.security; + +import lombok.*; + +import javax.persistence.*; +import java.util.Set; + +/** + * Created by jt on 6/21/20. + */ +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Entity +public class Authority { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Integer id; + + private String permission; + + @ManyToMany(mappedBy = "authorities") + private Set roles; +} diff --git a/src/main/java/guru/sfg/brewery/domain/security/LoginFailure.java b/src/main/java/guru/sfg/brewery/domain/security/LoginFailure.java new file mode 100644 index 000000000..dd7710f69 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/domain/security/LoginFailure.java @@ -0,0 +1,37 @@ +package guru.sfg.brewery.domain.security; + +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import javax.persistence.*; +import java.sql.Timestamp; + +/** + * Created by jt on 7/20/20. + */ +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +@Entity +public class LoginFailure { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Integer id; + + private String username; + + @ManyToOne + private User user; + + private String sourceIp; + + @CreationTimestamp + @Column(updatable = false) + private Timestamp createdDate; + + @UpdateTimestamp + private Timestamp lastModifiedDate; +} \ No newline at end of file diff --git a/src/main/java/guru/sfg/brewery/domain/security/LoginSuccess.java b/src/main/java/guru/sfg/brewery/domain/security/LoginSuccess.java new file mode 100644 index 000000000..2676f9b88 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/domain/security/LoginSuccess.java @@ -0,0 +1,36 @@ +package guru.sfg.brewery.domain.security; + +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import javax.persistence.*; +import java.sql.Timestamp; + +/** + * Created by jt on 7/20/20. + */ +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +@Entity +public class LoginSuccess { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Integer id; + + @ManyToOne + private User user; + + private String sourceIp; + + @CreationTimestamp + @Column(updatable = false) + private Timestamp createdDate; + + @UpdateTimestamp + private Timestamp lastModifiedDate; +} diff --git a/src/main/java/guru/sfg/brewery/domain/security/Role.java b/src/main/java/guru/sfg/brewery/domain/security/Role.java new file mode 100644 index 000000000..5c4ffe1f2 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/domain/security/Role.java @@ -0,0 +1,34 @@ +package guru.sfg.brewery.domain.security; + +import lombok.*; + +import javax.persistence.*; +import java.util.Set; + +/** + * Created by jt on 6/29/20. + */ +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Entity +public class Role { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Integer id; + + private String name; + + @ManyToMany(mappedBy = "roles") + private Set users; + + @Singular + @ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST}, fetch = FetchType.EAGER) + @JoinTable(name = "role_authority", + joinColumns = {@JoinColumn(name = "ROLE_ID", referencedColumnName = "ID")}, + inverseJoinColumns = {@JoinColumn(name = "AUTHORITY_ID", referencedColumnName = "ID")}) + private Set authorities; + +} diff --git a/src/main/java/guru/sfg/brewery/domain/security/User.java b/src/main/java/guru/sfg/brewery/domain/security/User.java new file mode 100644 index 000000000..b480bbc55 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/domain/security/User.java @@ -0,0 +1,99 @@ +package guru.sfg.brewery.domain.security; + +import guru.sfg.brewery.domain.Customer; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import org.springframework.security.core.CredentialsContainer; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import javax.persistence.*; +import java.sql.Timestamp; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Created by jt on 6/21/20. + */ +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Entity +public class User implements UserDetails, CredentialsContainer { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Integer id; + + private String username; + private String password; + + @Singular + @ManyToMany(cascade = {CascadeType.MERGE}, fetch = FetchType.EAGER) + @JoinTable(name = "user_role", + joinColumns = {@JoinColumn(name = "USER_ID", referencedColumnName = "ID")}, + inverseJoinColumns = {@JoinColumn(name = "ROLE_ID", referencedColumnName = "ID")}) + private Set roles; + + @ManyToOne(fetch = FetchType.EAGER) + private Customer customer; + + @Transient + public Set getAuthorities() { + return this.roles.stream() + .map(Role::getAuthorities) + .flatMap(Set::stream) + .map(authority -> { + return new SimpleGrantedAuthority(authority.getPermission()); + }) + .collect(Collectors.toSet()); + } + + @Override + public boolean isAccountNonExpired() { + return this.accountNonExpired; + } + + @Override + public boolean isAccountNonLocked() { + return this.accountNonLocked; + } + + @Override + public boolean isCredentialsNonExpired() { + return this.credentialsNonExpired; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } + + @Builder.Default + private Boolean accountNonExpired = true; + + @Builder.Default + private Boolean accountNonLocked = true; + + @Builder.Default + private Boolean credentialsNonExpired = true; + + @Builder.Default + private Boolean enabled = true; + + @Override + public void eraseCredentials() { + this.password = null; + } + + @CreationTimestamp + @Column(updatable = false) + private Timestamp createdDate; + + @UpdateTimestamp + private Timestamp lastModifiedDate; +} \ No newline at end of file diff --git a/src/main/java/guru/sfg/brewery/listener/AuthenticationFailureListener.java b/src/main/java/guru/sfg/brewery/listener/AuthenticationFailureListener.java new file mode 100644 index 000000000..a34414ce2 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/listener/AuthenticationFailureListener.java @@ -0,0 +1,78 @@ +package guru.sfg.brewery.listener; + +import guru.sfg.brewery.domain.security.LoginFailure; +import guru.sfg.brewery.domain.security.User; +import guru.sfg.brewery.repositories.security.LoginFailureRepository; +import guru.sfg.brewery.repositories.security.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent; +import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.stereotype.Component; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; + +/** + * Created by jt on 7/20/20. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class AuthenticationFailureListener { + + private final LoginFailureRepository loginFailureRepository; + private final UserRepository userRepository; + + @EventListener + public void listen(AuthenticationFailureBadCredentialsEvent event) { + log.debug("Login failure"); + + if (event.getSource() instanceof UsernamePasswordAuthenticationToken) { + UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) event.getSource(); + LoginFailure.LoginFailureBuilder builder = LoginFailure.builder(); + + if (token.getPrincipal() instanceof String) { + log.debug("Attempted Username: " + token.getPrincipal()); + builder.username((String) token.getPrincipal()); + userRepository.findByUsername((String) token.getPrincipal()).ifPresent(builder::user); + } + + if (token.getDetails() instanceof WebAuthenticationDetails) { + WebAuthenticationDetails details = (WebAuthenticationDetails) token.getDetails(); + + log.debug("Source IP: " + details.getRemoteAddress()); + builder.sourceIp(details.getRemoteAddress()); + } + LoginFailure failure = loginFailureRepository.save(builder.build()); + log.debug("Failure Event: " + failure.getId()); + + if (failure.getUser() != null) { + lockUserAccount(failure.getUser()); + } + } + + + } + + private void lockUserAccount(User user) { + List failures = loginFailureRepository.findAllByUserAndCreatedDateIsAfter(user, + Timestamp.valueOf(LocalDateTime.now().minusDays(1))); + + if(failures.size() > 3){ + log.debug("Locking User Account... "); + user.setAccountNonLocked(false); + userRepository.save(user); + } + } + + + + + + + +} diff --git a/src/main/java/guru/sfg/brewery/listener/AuthenticationSuccessListener.java b/src/main/java/guru/sfg/brewery/listener/AuthenticationSuccessListener.java new file mode 100644 index 000000000..73d278389 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/listener/AuthenticationSuccessListener.java @@ -0,0 +1,56 @@ +package guru.sfg.brewery.listener; + +import guru.sfg.brewery.domain.security.LoginSuccess; +import guru.sfg.brewery.domain.security.User; +import guru.sfg.brewery.repositories.security.LoginSuccessRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.event.AuthenticationSuccessEvent; +import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.stereotype.Component; + +/** + * Created by jt on 7/18/20. + */ +@Slf4j +@RequiredArgsConstructor +@Component +public class AuthenticationSuccessListener { + + private final LoginSuccessRepository loginSuccessRepository; + + @EventListener + public void listen(AuthenticationSuccessEvent event){ + + log.debug("User Logged In Okay"); + + if (event.getSource() instanceof UsernamePasswordAuthenticationToken) { + LoginSuccess.LoginSuccessBuilder builder = LoginSuccess.builder(); + + UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) event.getSource(); + + if(token.getPrincipal() instanceof User){ + User user = (User) token.getPrincipal(); + builder.user(user); + + log.debug("User name logged in: " + user.getUsername() ); + } + + if(token.getDetails() instanceof WebAuthenticationDetails){ + WebAuthenticationDetails details = (WebAuthenticationDetails) token.getDetails(); + + log.debug("Source IP: " + details.getRemoteAddress()); + builder.sourceIp(details.getRemoteAddress()); + } + + LoginSuccess loginSuccess = loginSuccessRepository.save(builder.build()); + + log.debug("Login Success saved. Id: " + loginSuccess.getId()); + } + + + + } +} diff --git a/src/main/java/guru/sfg/brewery/permission/BeerCreatePermission.java b/src/main/java/guru/sfg/brewery/permission/BeerCreatePermission.java new file mode 100644 index 000000000..4750ac4de --- /dev/null +++ b/src/main/java/guru/sfg/brewery/permission/BeerCreatePermission.java @@ -0,0 +1,14 @@ +package guru.sfg.brewery.permission; + +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Created by jt on 6/30/20. + */ +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAuthority('beer.create')") +public @interface BeerCreatePermission { +} diff --git a/src/main/java/guru/sfg/brewery/permission/BeerDeletePermission.java b/src/main/java/guru/sfg/brewery/permission/BeerDeletePermission.java new file mode 100644 index 000000000..a8f7d9959 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/permission/BeerDeletePermission.java @@ -0,0 +1,14 @@ +package guru.sfg.brewery.permission; + +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Created by jt on 6/30/20. + */ +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAuthority('beer.delete')") +public @interface BeerDeletePermission { +} diff --git a/src/main/java/guru/sfg/brewery/permission/BeerOrderCreatePermission.java b/src/main/java/guru/sfg/brewery/permission/BeerOrderCreatePermission.java new file mode 100644 index 000000000..077edb5d0 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/permission/BeerOrderCreatePermission.java @@ -0,0 +1,16 @@ +package guru.sfg.brewery.permission; + +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Created by jt on 7/7/20. + */ +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAuthority('order.create') OR " + + "hasAuthority('customer.order.create') " + + " AND @beerOrderAuthenticationManger.customerIdMatches(authentication, #customerId )") +public @interface BeerOrderCreatePermission { +} diff --git a/src/main/java/guru/sfg/brewery/permission/BeerOrderPickupPermission.java b/src/main/java/guru/sfg/brewery/permission/BeerOrderPickupPermission.java new file mode 100644 index 000000000..58bb62065 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/permission/BeerOrderPickupPermission.java @@ -0,0 +1,16 @@ +package guru.sfg.brewery.permission; + +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Created by jt on 7/7/20. + */ +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAuthority('order.pickup') OR " + + "hasAuthority('customer.order.pickup') " + + " AND @beerOrderAuthenticationManger.customerIdMatches(authentication, #customerId )") +public @interface BeerOrderPickupPermission { +} diff --git a/src/main/java/guru/sfg/brewery/permission/BeerOrderReadPermission.java b/src/main/java/guru/sfg/brewery/permission/BeerOrderReadPermission.java new file mode 100644 index 000000000..79038eb7e --- /dev/null +++ b/src/main/java/guru/sfg/brewery/permission/BeerOrderReadPermission.java @@ -0,0 +1,16 @@ +package guru.sfg.brewery.permission; + +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Created by jt on 7/7/20. + */ +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAuthority('order.read') OR " + + "hasAuthority('customer.order.read') " + + " AND @beerOrderAuthenticationManger.customerIdMatches(authentication, #customerId )") +public @interface BeerOrderReadPermission { +} diff --git a/src/main/java/guru/sfg/brewery/permission/BeerOrderReadPermissionV2.java b/src/main/java/guru/sfg/brewery/permission/BeerOrderReadPermissionV2.java new file mode 100644 index 000000000..fee96fc34 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/permission/BeerOrderReadPermissionV2.java @@ -0,0 +1,14 @@ +package guru.sfg.brewery.permission; +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Created by jt on 7/7/20. + */ + +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAnyAuthority('order.read', 'customer.order.read')") +public @interface BeerOrderReadPermissionV2 { +} diff --git a/src/main/java/guru/sfg/brewery/permission/BeerReadPermission.java b/src/main/java/guru/sfg/brewery/permission/BeerReadPermission.java new file mode 100644 index 000000000..ece56cc43 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/permission/BeerReadPermission.java @@ -0,0 +1,14 @@ +package guru.sfg.brewery.permission; + +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Created by jt on 6/30/20. + */ +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAuthority('beer.read')") +public @interface BeerReadPermission { +} diff --git a/src/main/java/guru/sfg/brewery/permission/BeerUpdatePermission.java b/src/main/java/guru/sfg/brewery/permission/BeerUpdatePermission.java new file mode 100644 index 000000000..cf2dc28a4 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/permission/BeerUpdatePermission.java @@ -0,0 +1,14 @@ +package guru.sfg.brewery.permission; + +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Created by jt on 6/30/20. + */ +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAuthority('beer.update')") +public @interface BeerUpdatePermission { +} diff --git a/src/main/java/guru/sfg/brewery/permission/BreweryCreatePermission.java b/src/main/java/guru/sfg/brewery/permission/BreweryCreatePermission.java new file mode 100644 index 000000000..d9e744a4b --- /dev/null +++ b/src/main/java/guru/sfg/brewery/permission/BreweryCreatePermission.java @@ -0,0 +1,12 @@ +package guru.sfg.brewery.permission; + + +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAuthority('brewery.create')") +public @interface BreweryCreatePermission { +} diff --git a/src/main/java/guru/sfg/brewery/permission/BreweryDeletePermission.java b/src/main/java/guru/sfg/brewery/permission/BreweryDeletePermission.java new file mode 100644 index 000000000..126a4f45a --- /dev/null +++ b/src/main/java/guru/sfg/brewery/permission/BreweryDeletePermission.java @@ -0,0 +1,11 @@ +package guru.sfg.brewery.permission; + +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAuthority('brewery.delete')") +public @interface BreweryDeletePermission { +} diff --git a/src/main/java/guru/sfg/brewery/permission/BreweryReadPermission.java b/src/main/java/guru/sfg/brewery/permission/BreweryReadPermission.java new file mode 100644 index 000000000..58754e084 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/permission/BreweryReadPermission.java @@ -0,0 +1,11 @@ +package guru.sfg.brewery.permission; + +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAuthority('brewery.read')") +public @interface BreweryReadPermission { +} diff --git a/src/main/java/guru/sfg/brewery/permission/BreweryUpdatePermission.java b/src/main/java/guru/sfg/brewery/permission/BreweryUpdatePermission.java new file mode 100644 index 000000000..9b41b1836 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/permission/BreweryUpdatePermission.java @@ -0,0 +1,12 @@ +package guru.sfg.brewery.permission; + + +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAuthority('brewery.update')") +public @interface BreweryUpdatePermission { +} diff --git a/src/main/java/guru/sfg/brewery/permission/CustomerCreatePermission.java b/src/main/java/guru/sfg/brewery/permission/CustomerCreatePermission.java new file mode 100644 index 000000000..5ce2a6c74 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/permission/CustomerCreatePermission.java @@ -0,0 +1,12 @@ +package guru.sfg.brewery.permission; + + +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAuthority('customer.create')") +public @interface CustomerCreatePermission { +} diff --git a/src/main/java/guru/sfg/brewery/permission/CustomerDeletePermission.java b/src/main/java/guru/sfg/brewery/permission/CustomerDeletePermission.java new file mode 100644 index 000000000..6b7aab9b3 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/permission/CustomerDeletePermission.java @@ -0,0 +1,11 @@ +package guru.sfg.brewery.permission; + +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAuthority('customer.delete')") +public @interface CustomerDeletePermission { +} diff --git a/src/main/java/guru/sfg/brewery/permission/CustomerReadPermission.java b/src/main/java/guru/sfg/brewery/permission/CustomerReadPermission.java new file mode 100644 index 000000000..35cbcec4a --- /dev/null +++ b/src/main/java/guru/sfg/brewery/permission/CustomerReadPermission.java @@ -0,0 +1,10 @@ +package guru.sfg.brewery.permission; +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAuthority('customer.read')") +public @interface CustomerReadPermission { +} diff --git a/src/main/java/guru/sfg/brewery/permission/CustomerUpdatePermission.java b/src/main/java/guru/sfg/brewery/permission/CustomerUpdatePermission.java new file mode 100644 index 000000000..fa4572b4a --- /dev/null +++ b/src/main/java/guru/sfg/brewery/permission/CustomerUpdatePermission.java @@ -0,0 +1,11 @@ +package guru.sfg.brewery.permission; + +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAuthority('customer.update')") +public @interface CustomerUpdatePermission { +} diff --git a/src/main/java/guru/sfg/brewery/repositories/BeerOrderRepository.java b/src/main/java/guru/sfg/brewery/repositories/BeerOrderRepository.java index 80fd28c07..671a82e16 100644 --- a/src/main/java/guru/sfg/brewery/repositories/BeerOrderRepository.java +++ b/src/main/java/guru/sfg/brewery/repositories/BeerOrderRepository.java @@ -23,6 +23,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; import javax.persistence.LockModeType; import java.util.List; @@ -39,4 +40,8 @@ public interface BeerOrderRepository extends JpaRepository { @Lock(LockModeType.PESSIMISTIC_WRITE) BeerOrder findOneById(UUID id); + + @Query("select o from BeerOrder o where o.id =?1 and " + + "(true = :#{hasAuthority('order.read')} or o.customer.id = ?#{principal?.customer?.id})") + BeerOrder findOrderByIdSecure(UUID orderId); } diff --git a/src/main/java/guru/sfg/brewery/repositories/CustomerRepository.java b/src/main/java/guru/sfg/brewery/repositories/CustomerRepository.java index 5a634ca38..abe065ce7 100644 --- a/src/main/java/guru/sfg/brewery/repositories/CustomerRepository.java +++ b/src/main/java/guru/sfg/brewery/repositories/CustomerRepository.java @@ -17,9 +17,11 @@ package guru.sfg.brewery.repositories; import guru.sfg.brewery.domain.Customer; +import guru.sfg.brewery.domain.security.Role; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; import java.util.UUID; /** @@ -27,4 +29,6 @@ */ public interface CustomerRepository extends JpaRepository { List findAllByCustomerNameLike(String customerName); + + Optional findAllByCustomerName(String customerName); } diff --git a/src/main/java/guru/sfg/brewery/repositories/security/AuthorityRepository.java b/src/main/java/guru/sfg/brewery/repositories/security/AuthorityRepository.java new file mode 100644 index 000000000..86cf4efa0 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/repositories/security/AuthorityRepository.java @@ -0,0 +1,10 @@ +package guru.sfg.brewery.repositories.security; + +import guru.sfg.brewery.domain.security.Authority; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * Created by jt on 6/21/20. + */ +public interface AuthorityRepository extends JpaRepository { +} diff --git a/src/main/java/guru/sfg/brewery/repositories/security/LoginFailureRepository.java b/src/main/java/guru/sfg/brewery/repositories/security/LoginFailureRepository.java new file mode 100644 index 000000000..f192bc89a --- /dev/null +++ b/src/main/java/guru/sfg/brewery/repositories/security/LoginFailureRepository.java @@ -0,0 +1,16 @@ +package guru.sfg.brewery.repositories.security; + +import guru.sfg.brewery.domain.security.LoginFailure; +import guru.sfg.brewery.domain.security.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.sql.Timestamp; +import java.util.List; + +/** + * Created by jt on 7/20/20. + */ +public interface LoginFailureRepository extends JpaRepository { + + List findAllByUserAndCreatedDateIsAfter(User user, Timestamp timestamp); +} diff --git a/src/main/java/guru/sfg/brewery/repositories/security/LoginSuccessRepository.java b/src/main/java/guru/sfg/brewery/repositories/security/LoginSuccessRepository.java new file mode 100644 index 000000000..b196113aa --- /dev/null +++ b/src/main/java/guru/sfg/brewery/repositories/security/LoginSuccessRepository.java @@ -0,0 +1,10 @@ +package guru.sfg.brewery.repositories.security; + +import guru.sfg.brewery.domain.security.LoginSuccess; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * Created by jt on 7/20/20. + */ +public interface LoginSuccessRepository extends JpaRepository { +} diff --git a/src/main/java/guru/sfg/brewery/repositories/security/RoleRepository.java b/src/main/java/guru/sfg/brewery/repositories/security/RoleRepository.java new file mode 100644 index 000000000..88ad96ccf --- /dev/null +++ b/src/main/java/guru/sfg/brewery/repositories/security/RoleRepository.java @@ -0,0 +1,13 @@ +package guru.sfg.brewery.repositories.security; + +import guru.sfg.brewery.domain.security.Role; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +/** + * Created by jt on 6/29/20. + */ +public interface RoleRepository extends JpaRepository { + Optional findByName(String customer); +} diff --git a/src/main/java/guru/sfg/brewery/repositories/security/UserRepository.java b/src/main/java/guru/sfg/brewery/repositories/security/UserRepository.java new file mode 100644 index 000000000..7a26122c3 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/repositories/security/UserRepository.java @@ -0,0 +1,14 @@ +package guru.sfg.brewery.repositories.security; + +import guru.sfg.brewery.domain.security.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.sql.Timestamp; +import java.util.List; +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); + + List findAllByAccountNonLockedAndLastModifiedDateIsBefore(Boolean locked, Timestamp timestamp); +} \ No newline at end of file diff --git a/src/main/java/guru/sfg/brewery/services/BeerOrderService.java b/src/main/java/guru/sfg/brewery/services/BeerOrderService.java index 082b132be..ae5ec9b9d 100644 --- a/src/main/java/guru/sfg/brewery/services/BeerOrderService.java +++ b/src/main/java/guru/sfg/brewery/services/BeerOrderService.java @@ -32,4 +32,8 @@ public interface BeerOrderService { BeerOrderDto getOrderById(UUID customerId, UUID orderId); void pickupOrder(UUID customerId, UUID orderId); + + BeerOrderPagedList listOrders(Pageable pageable); + + BeerOrderDto getOrderById(UUID orderId); } diff --git a/src/main/java/guru/sfg/brewery/services/BeerOrderServiceImpl.java b/src/main/java/guru/sfg/brewery/services/BeerOrderServiceImpl.java index 559250a0f..25b9a19e9 100644 --- a/src/main/java/guru/sfg/brewery/services/BeerOrderServiceImpl.java +++ b/src/main/java/guru/sfg/brewery/services/BeerOrderServiceImpl.java @@ -1,20 +1,3 @@ -/* - * Copyright 2020 the original author or authors. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - package guru.sfg.brewery.services; import guru.sfg.brewery.domain.BeerOrder; @@ -66,6 +49,19 @@ public BeerOrderPagedList listOrders(UUID customerId, Pageable pageable) { } } + @Override + public BeerOrderPagedList listOrders(Pageable pageable) { + Page beerOrderPage = beerOrderRepository.findAll(pageable); + + return new BeerOrderPagedList(beerOrderPage + .stream() + .map(beerOrderMapper::beerOrderToDto) + .collect(Collectors.toList()), PageRequest.of( + beerOrderPage.getPageable().getPageNumber(), + beerOrderPage.getPageable().getPageSize()), + beerOrderPage.getTotalElements()); + } + @Transactional @Override public BeerOrderDto placeOrder(UUID customerId, BeerOrderDto beerOrderDto) { @@ -94,6 +90,13 @@ public BeerOrderDto getOrderById(UUID customerId, UUID orderId) { return beerOrderMapper.beerOrderToDto(getOrder(customerId, orderId)); } + @Override + public BeerOrderDto getOrderById(UUID orderId) { + BeerOrder beerOrder = beerOrderRepository.findOrderByIdSecure(orderId); + + return beerOrderMapper.beerOrderToDto(beerOrder); + } + @Override public void pickupOrder(UUID customerId, UUID orderId) { BeerOrder beerOrder = getOrder(customerId, orderId); @@ -120,4 +123,4 @@ private BeerOrder getOrder(UUID customerId, UUID orderId){ } throw new RuntimeException("Customer Not Found"); } -} +} \ No newline at end of file diff --git a/src/main/java/guru/sfg/brewery/services/UserUnlockService.java b/src/main/java/guru/sfg/brewery/services/UserUnlockService.java new file mode 100644 index 000000000..6991586ac --- /dev/null +++ b/src/main/java/guru/sfg/brewery/services/UserUnlockService.java @@ -0,0 +1,37 @@ +package guru.sfg.brewery.services; + +import guru.sfg.brewery.domain.security.User; +import guru.sfg.brewery.repositories.security.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@Service +public class UserUnlockService { + + private final UserRepository userRepository; + + @Scheduled(fixedRate = 5000) + public void unlockAccounts(){ + log.debug("Running Unlock Accounts"); + + List lockedUsers = userRepository + .findAllByAccountNonLockedAndLastModifiedDateIsBefore(false, + Timestamp.valueOf(LocalDateTime.now().minusSeconds(30))); + + if(lockedUsers.size() > 0){ + log.debug("Locked Accounts Found, Unlocking"); + lockedUsers.forEach(user -> user.setAccountNonLocked(true)); + + userRepository.saveAll(lockedUsers); + } + } + +} \ No newline at end of file diff --git a/src/main/java/guru/sfg/brewery/services/security/JpaUserDetailsService.java b/src/main/java/guru/sfg/brewery/services/security/JpaUserDetailsService.java new file mode 100644 index 000000000..ff0d429b3 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/services/security/JpaUserDetailsService.java @@ -0,0 +1,41 @@ +package guru.sfg.brewery.services.security; + +import guru.sfg.brewery.domain.security.Authority; +import guru.sfg.brewery.repositories.security.UserRepository; +import lombok.AllArgsConstructor; +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.stereotype.Service; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +@AllArgsConstructor +public class JpaUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return userRepository.findByUsername(username).orElseThrow(() -> { + return new UsernameNotFoundException("User name: " + username + " not found"); + }); + } + + private Collection convertToSpringAuthorities(Set authorities) { + if (authorities != null && authorities.size() > 0) { + return authorities.stream() + .map(Authority::getPermission) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + } else { + return new HashSet<>(); + } + } +} diff --git a/src/main/java/guru/sfg/brewery/web/controllers/BeerController.java b/src/main/java/guru/sfg/brewery/web/controllers/BeerController.java index ee5d9cdd5..ef0060f8f 100644 --- a/src/main/java/guru/sfg/brewery/web/controllers/BeerController.java +++ b/src/main/java/guru/sfg/brewery/web/controllers/BeerController.java @@ -19,6 +19,9 @@ import guru.sfg.brewery.domain.Beer; +import guru.sfg.brewery.permission.BeerCreatePermission; +import guru.sfg.brewery.permission.BeerReadPermission; +import guru.sfg.brewery.permission.BeerUpdatePermission; import guru.sfg.brewery.repositories.BeerInventoryRepository; import guru.sfg.brewery.repositories.BeerRepository; import lombok.RequiredArgsConstructor; @@ -48,12 +51,14 @@ public class BeerController { private final BeerInventoryRepository beerInventoryRepository; + @BeerReadPermission @RequestMapping("/find") public String findBeers(Model model) { model.addAttribute("beer", Beer.builder().build()); return "beers/findBeers"; } + @BeerReadPermission @GetMapping public String processFindFormReturnMany(Beer beer, BindingResult result, Model model) { // find beers by name @@ -77,6 +82,7 @@ public String processFindFormReturnMany(Beer beer, BindingResult result, Model m } + @BeerReadPermission @GetMapping("/{beerId}") public ModelAndView showBeer(@PathVariable UUID beerId) { ModelAndView mav = new ModelAndView("beers/beerDetails"); @@ -85,12 +91,14 @@ public ModelAndView showBeer(@PathVariable UUID beerId) { return mav; } + @BeerCreatePermission @GetMapping("/new") public String initCreationForm(Model model) { model.addAttribute("beer", Beer.builder().build()); return "beers/createBeer"; } + @BeerCreatePermission @PostMapping("/new") public String processCreationForm(Beer beer) { //ToDO: Add Service @@ -107,6 +115,7 @@ public String processCreationForm(Beer beer) { return "redirect:/beers/" + savedBeer.getId(); } + @BeerUpdatePermission @GetMapping("/{beerId}/edit") public String initUpdateBeerForm(@PathVariable UUID beerId, Model model) { if (beerRepository.findById(beerId).isPresent()) @@ -114,6 +123,7 @@ public String initUpdateBeerForm(@PathVariable UUID beerId, Model model) { return "beers/createOrUpdateBeer"; } + @BeerUpdatePermission @PostMapping("/{beerId}/edit") public String processUpdateForm(@Valid Beer beer, BindingResult result) { if (result.hasErrors()) { diff --git a/src/main/java/guru/sfg/brewery/web/controllers/BreweryController.java b/src/main/java/guru/sfg/brewery/web/controllers/BreweryController.java index 82d2f9226..9dbdc1adf 100644 --- a/src/main/java/guru/sfg/brewery/web/controllers/BreweryController.java +++ b/src/main/java/guru/sfg/brewery/web/controllers/BreweryController.java @@ -18,6 +18,7 @@ package guru.sfg.brewery.web.controllers; import guru.sfg.brewery.domain.Brewery; +import guru.sfg.brewery.permission.BreweryReadPermission; import guru.sfg.brewery.services.BreweryService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; @@ -36,14 +37,16 @@ public class BreweryController { private final BreweryService breweryService; + @BreweryReadPermission @GetMapping({"/breweries", "/breweries/index", "/breweries/index.html", "/breweries.html"}) public String listBreweries(Model model) { model.addAttribute("breweries", breweryService.getAllBreweries()); return "breweries/index"; } + + @BreweryReadPermission @GetMapping("/api/v1/breweries") - public @ResponseBody - List getBreweriesJson(){ + public @ResponseBody List getBreweriesJson(){ return breweryService.getAllBreweries(); } } diff --git a/src/main/java/guru/sfg/brewery/web/controllers/CustomerController.java b/src/main/java/guru/sfg/brewery/web/controllers/CustomerController.java index 06fef9090..64bb09799 100644 --- a/src/main/java/guru/sfg/brewery/web/controllers/CustomerController.java +++ b/src/main/java/guru/sfg/brewery/web/controllers/CustomerController.java @@ -18,6 +18,9 @@ package guru.sfg.brewery.web.controllers; import guru.sfg.brewery.domain.Customer; +import guru.sfg.brewery.permission.CustomerCreatePermission; +import guru.sfg.brewery.permission.CustomerReadPermission; +import guru.sfg.brewery.permission.CustomerUpdatePermission; import guru.sfg.brewery.repositories.CustomerRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; @@ -41,14 +44,16 @@ public class CustomerController { //ToDO: Add service private final CustomerRepository customerRepository; + @CustomerReadPermission @RequestMapping("/find") - public String findCustomers(Model model){ + public String findCustomers(Model model) { model.addAttribute("customer", Customer.builder().build()); return "customers/findCustomers"; } + @CustomerReadPermission @GetMapping - public String processFindFormReturnMany(Customer customer, BindingResult result, Model model){ + public String processFindFormReturnMany(Customer customer, BindingResult result, Model model) { // find customers by name //ToDO: Add Service List customers = customerRepository.findAllByCustomerNameLike("%" + customer.getCustomerName() + "%"); @@ -67,7 +72,8 @@ public String processFindFormReturnMany(Customer customer, BindingResult result, } } - @GetMapping("/{customerId}") + @CustomerReadPermission + @GetMapping("/{customerId}") public ModelAndView showCustomer(@PathVariable UUID customerId) { ModelAndView mav = new ModelAndView("customers/customerDetails"); //ToDO: Add Service @@ -75,12 +81,14 @@ public ModelAndView showCustomer(@PathVariable UUID customerId) { return mav; } + @CustomerCreatePermission @GetMapping("/new") public String initCreationForm(Model model) { model.addAttribute("customer", Customer.builder().build()); return "customers/createCustomer"; } + @CustomerCreatePermission @PostMapping("/new") public String processCreationForm(Customer customer) { //ToDO: Add Service @@ -88,24 +96,26 @@ public String processCreationForm(Customer customer) { .customerName(customer.getCustomerName()) .build(); - Customer savedCustomer= customerRepository.save(newCustomer); + Customer savedCustomer = customerRepository.save(newCustomer); return "redirect:/customers/" + savedCustomer.getId(); } + @CustomerUpdatePermission @GetMapping("/{customerId}/edit") - public String initUpdateCustomerForm(@PathVariable UUID customerId, Model model) { - if(customerRepository.findById(customerId).isPresent()) - model.addAttribute("customer", customerRepository.findById(customerId).get()); - return "customers/createOrUpdateCustomer"; - } + public String initUpdateCustomerForm(@PathVariable UUID customerId, Model model) { + if (customerRepository.findById(customerId).isPresent()) + model.addAttribute("customer", customerRepository.findById(customerId).get()); + return "customers/createOrUpdateCustomer"; + } + @CustomerUpdatePermission @PostMapping("/{beerId}/edit") public String processUpdationForm(@Valid Customer customer, BindingResult result) { if (result.hasErrors()) { return "beers/createOrUpdateCustomer"; } else { //ToDO: Add Service - Customer savedCustomer = customerRepository.save(customer); + Customer savedCustomer = customerRepository.save(customer); return "redirect:/customers/" + savedCustomer.getId(); } } diff --git a/src/main/java/guru/sfg/brewery/web/controllers/api/BeerOrderController.java b/src/main/java/guru/sfg/brewery/web/controllers/api/BeerOrderController.java new file mode 100644 index 000000000..1a1c42fcf --- /dev/null +++ b/src/main/java/guru/sfg/brewery/web/controllers/api/BeerOrderController.java @@ -0,0 +1,84 @@ +/* + * Copyright 2019 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package guru.sfg.brewery.web.controllers.api; + +import guru.sfg.brewery.permission.BeerOrderCreatePermission; +import guru.sfg.brewery.permission.BeerOrderPickupPermission; +import guru.sfg.brewery.permission.BeerOrderReadPermission; +import guru.sfg.brewery.services.BeerOrderService; +import guru.sfg.brewery.web.model.BeerOrderDto; +import guru.sfg.brewery.web.model.BeerOrderPagedList; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +/** + * Beer Order Controller + */ +@RequestMapping("/api/v1/customers/{customerId}/") +@RestController +public class BeerOrderController { + + private static final Integer DEFAULT_PAGE_NUMBER = 0; + private static final Integer DEFAULT_PAGE_SIZE = 25; + + private final BeerOrderService beerOrderService; + + public BeerOrderController(BeerOrderService beerOrderService) { + this.beerOrderService = beerOrderService; + } + + @BeerOrderReadPermission + @GetMapping("orders") + public BeerOrderPagedList listOrders(@PathVariable("customerId") UUID customerId, + @RequestParam(value = "pageNumber", required = false) Integer pageNumber, + @RequestParam(value = "pageSize", required = false) Integer pageSize) { + + if (pageNumber == null || pageNumber < 0) { + pageNumber = DEFAULT_PAGE_NUMBER; + } + + if (pageSize == null || pageSize < 1) { + pageSize = DEFAULT_PAGE_SIZE; + } + + return beerOrderService.listOrders(customerId, PageRequest.of(pageNumber, pageSize)); + } + + @BeerOrderCreatePermission + @PostMapping("orders") + @ResponseStatus(HttpStatus.CREATED) + public BeerOrderDto placeOrder(@PathVariable("customerId") UUID customerId, @RequestBody BeerOrderDto beerOrderDto) { + return beerOrderService.placeOrder(customerId, beerOrderDto); + } + + @BeerOrderReadPermission + @GetMapping("orders/{orderId}") + public BeerOrderDto getOrder(@PathVariable("customerId") UUID customerId, @PathVariable("orderId") UUID orderId) { + return beerOrderService.getOrderById(customerId, orderId); + } + + @BeerOrderPickupPermission + @PutMapping("/orders/{orderId}/pickup") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void pickupOrder(@PathVariable("customerId") UUID customerId, @PathVariable("orderId") UUID orderId) { + beerOrderService.pickupOrder(customerId, orderId); + } +} diff --git a/src/main/java/guru/sfg/brewery/web/controllers/api/BeerOrderControllerV2.java b/src/main/java/guru/sfg/brewery/web/controllers/api/BeerOrderControllerV2.java new file mode 100644 index 000000000..d36f2e7a5 --- /dev/null +++ b/src/main/java/guru/sfg/brewery/web/controllers/api/BeerOrderControllerV2.java @@ -0,0 +1,63 @@ +package guru.sfg.brewery.web.controllers.api; + +import guru.sfg.brewery.domain.security.User; +import guru.sfg.brewery.permission.BeerOrderReadPermissionV2; +import guru.sfg.brewery.services.BeerOrderService; +import guru.sfg.brewery.web.model.BeerOrderDto; +import guru.sfg.brewery.web.model.BeerOrderPagedList; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.util.UUID; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v2/orders/") +public class BeerOrderControllerV2 { + + private static final Integer DEFAULT_PAGE_NUMBER = 0; + private static final Integer DEFAULT_PAGE_SIZE = 25; + + private final BeerOrderService beerOrderService; + + @BeerOrderReadPermissionV2 + @GetMapping + public BeerOrderPagedList listOrders(@AuthenticationPrincipal User user, + @RequestParam(value = "pageNumber", required = false) Integer pageNumber, + @RequestParam(value = "pageSize", required = false) Integer pageSize){ + + if (pageNumber == null || pageNumber < 0){ + pageNumber = DEFAULT_PAGE_NUMBER; + } + + if (pageSize == null || pageSize < 1) { + pageSize = DEFAULT_PAGE_SIZE; + } + + if (user.getCustomer() != null) { + return beerOrderService.listOrders(user.getCustomer().getId(), PageRequest.of(pageNumber, pageSize)); + } else { + return beerOrderService.listOrders(PageRequest.of(pageNumber, pageSize)); + } + } + + @BeerOrderReadPermissionV2 + @GetMapping("{orderId}") + public BeerOrderDto getOrder(@PathVariable("orderId") UUID orderId){ + BeerOrderDto beerOrderDto = beerOrderService.getOrderById(orderId); + + if (beerOrderDto == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Order Not Found"); + } + + log.debug("Found Order: " + beerOrderDto); + + return beerOrderDto; + } +} diff --git a/src/main/java/guru/sfg/brewery/web/controllers/api/BeerRestController.java b/src/main/java/guru/sfg/brewery/web/controllers/api/BeerRestController.java index 057ff9a81..1d669089e 100644 --- a/src/main/java/guru/sfg/brewery/web/controllers/api/BeerRestController.java +++ b/src/main/java/guru/sfg/brewery/web/controllers/api/BeerRestController.java @@ -17,6 +17,10 @@ package guru.sfg.brewery.web.controllers.api; +import guru.sfg.brewery.permission.BeerCreatePermission; +import guru.sfg.brewery.permission.BeerDeletePermission; +import guru.sfg.brewery.permission.BeerReadPermission; +import guru.sfg.brewery.permission.BeerUpdatePermission; import guru.sfg.brewery.services.BeerService; import guru.sfg.brewery.web.model.BeerDto; import guru.sfg.brewery.web.model.BeerPagedList; @@ -46,6 +50,7 @@ public class BeerRestController { private final BeerService beerService; + @BeerReadPermission @GetMapping(produces = { "application/json" }, path = "beer") public ResponseEntity listBeers(@RequestParam(value = "pageNumber", required = false) Integer pageNumber, @RequestParam(value = "pageSize", required = false) Integer pageSize, @@ -72,6 +77,7 @@ public ResponseEntity listBeers(@RequestParam(value = "pageNumber return new ResponseEntity<>(beerList, HttpStatus.OK); } + @BeerReadPermission @GetMapping(path = {"beer/{beerId}"}, produces = { "application/json" }) public ResponseEntity getBeerById(@PathVariable("beerId") UUID beerId, @RequestParam(value = "showInventoryOnHand", required = false) Boolean showInventoryOnHand){ @@ -85,11 +91,13 @@ public ResponseEntity getBeerById(@PathVariable("beerId") UUID beerId, return new ResponseEntity<>(beerService.findBeerById(beerId, showInventoryOnHand), HttpStatus.OK); } + @BeerReadPermission @GetMapping(path = {"beerUpc/{upc}"}, produces = { "application/json" }) public ResponseEntity getBeerByUpc(@PathVariable("upc") String upc){ return new ResponseEntity<>(beerService.findBeerByUpc(upc), HttpStatus.OK); } + @BeerCreatePermission @PostMapping(path = "beer") public ResponseEntity saveNewBeer(@Valid @RequestBody BeerDto beerDto){ @@ -103,6 +111,7 @@ public ResponseEntity saveNewBeer(@Valid @RequestBody BeerDto beerDto){ return new ResponseEntity(httpHeaders, HttpStatus.CREATED); } + @BeerUpdatePermission @PutMapping(path = {"beer/{beerId}"}, produces = { "application/json" }) public ResponseEntity updateBeer(@PathVariable("beerId") UUID beerId, @Valid @RequestBody BeerDto beerDto){ @@ -111,6 +120,7 @@ public ResponseEntity updateBeer(@PathVariable("beerId") UUID beerId, @Valid @Re return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + @BeerDeletePermission @DeleteMapping({"beer/{beerId}"}) @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteBeer(@PathVariable("beerId") UUID beerId){ diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 912080aaa..13ff89a5d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -20,7 +20,22 @@ spring.jackson.serialization.write-date-timestamps-as-nanoseconds=true server.port=8080 spring.messages.basename=messages/messages + #h2 properties +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +hibernate.hbm2ddl.auto=create-drop + + logging.level.guru=debug # Spring Data hangs when not set under Spring Boot 2.3.0 -spring.data.jpa.repositories.bootstrap-mode=default \ No newline at end of file +spring.data.jpa.repositories.bootstrap-mode=default + +#spring.security.user.name=spring +#spring.security.user.password=password + +logging.level.guru.sfg.brewery=debug +logging.level.org.springframework.security=debug \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 000000000..012fb1f3a --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,4 @@ +create table persistent_logins (username varchar(64) not null, + series varchar(64) primary key, + token varchar(64) not null, + last_used timestamp not null); \ No newline at end of file diff --git a/src/main/resources/templates/beers/findBeers.html b/src/main/resources/templates/beers/findBeers.html index ce0bf55c1..de7bdc98f 100644 --- a/src/main/resources/templates/beers/findBeers.html +++ b/src/main/resources/templates/beers/findBeers.html @@ -1,4 +1,5 @@ @@ -29,7 +30,7 @@

Find Beers


-Add Beer +Add Beer - + \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 42684fb75..fbaa0237f 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -13,11 +13,12 @@ ~ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . - --> - + SFG Brewery @@ -25,6 +26,33 @@

SFG Brewery Monolith

+
+
+
Invalid Username or Password
+ +
User Account is Locked.
+ +
You Have Logged Out
+
+ + + + + + + +
+
+
+
+
+

User

+

Logout

+
+
+ + +
@@ -32,4 +60,4 @@

SFG Brewery Monolith

- + \ No newline at end of file diff --git a/src/test/java/guru/sfg/brewery/web/controllers/BaseIT.java b/src/test/java/guru/sfg/brewery/web/controllers/BaseIT.java new file mode 100644 index 000000000..fc26fdfcb --- /dev/null +++ b/src/test/java/guru/sfg/brewery/web/controllers/BaseIT.java @@ -0,0 +1,46 @@ +package guru.sfg.brewery.web.controllers; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.provider.Arguments; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.util.stream.Stream; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; + +/** + * Created by jt on 6/13/20. + */ +public abstract class BaseIT { + @Autowired + WebApplicationContext wac; + + protected MockMvc mockMvc; + + @BeforeEach + public void setup() { + mockMvc = MockMvcBuilders + .webAppContextSetup(wac) + .apply(springSecurity()) + .build(); + } + + public static Stream getStreamAdminCustomer() { + return Stream.of(Arguments.of("spring" , "guru"), + Arguments.of("scott", "password")); + } + + public static Stream getStreamAllUsers() { + return Stream.of(Arguments.of("spring" , "guru"), + Arguments.of("scott", "password"), + Arguments.of("user", "password")); + } + + public static Stream getStreamNotAdmin() { + return Stream.of(Arguments.of("scott", "password"), + Arguments.of("user", "password")); + } +} diff --git a/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java b/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java new file mode 100644 index 000000000..94615eec3 --- /dev/null +++ b/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java @@ -0,0 +1,109 @@ +package guru.sfg.brewery.web.controllers; + +import guru.sfg.brewery.domain.Beer; +import guru.sfg.brewery.repositories.BeerRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * Created by jt on 6/12/20. + */ +@SpringBootTest +public class BeerControllerIT extends BaseIT{ + + @Autowired + BeerRepository beerRepository; + + @DisplayName("Init New Form") + @Nested + class InitNewForm{ + + @ParameterizedTest(name = "#{index} with [{arguments}]") + @MethodSource("guru.sfg.brewery.web.controllers.BeerControllerIT#getStreamAllUsers") + void initCreationFormAuth(String user, String pwd) throws Exception { + + mockMvc.perform(get("/beers/new").with(httpBasic(user, pwd))) + .andExpect(status().isOk()) + .andExpect(view().name("beers/createBeer")) + .andExpect(model().attributeExists("beer")); + } + + @Test + void initCreationFormNotAuth() throws Exception { + mockMvc.perform(get("/beers/new")) + .andExpect(status().isUnauthorized()); + } + } + + @DisplayName("Init Find Beer Form") + @Nested + class FindForm{ + @ParameterizedTest(name = "#{index} with [{arguments}]") + @MethodSource("guru.sfg.brewery.web.controllers.BeerControllerIT#getStreamAllUsers") + void findBeersFormAUTH(String user, String pwd) throws Exception{ + mockMvc.perform(get("/beers/find") + .with(httpBasic(user, pwd))) + .andExpect(status().isOk()) + .andExpect(view().name("beers/findBeers")) + .andExpect(model().attributeExists("beer")); + } + + @Test + void findBeersWithAnonymous() throws Exception{ + mockMvc.perform(get("/beers/find").with(anonymous())) + .andExpect(status().isUnauthorized()); + } + } + + @DisplayName("Process Find Beer Form") + @Nested + class ProcessFindForm{ + @Test + void findBeerForm() throws Exception { + mockMvc.perform(get("/beers").param("beerName", "")) + .andExpect(status().isUnauthorized()); + } + + @ParameterizedTest(name = "#{index} with [{arguments}]") + @MethodSource("guru.sfg.brewery.web.controllers.BeerControllerIT#getStreamAllUsers") + void findBeerFormAuth(String user, String pwd) throws Exception { + mockMvc.perform(get("/beers").param("beerName", "") + .with(httpBasic(user, pwd))) + .andExpect(status().isOk()); + } + } + + @DisplayName("Get Beer By Id") + @Nested + class GetByID { + @ParameterizedTest(name = "#{index} with [{arguments}]") + @MethodSource("guru.sfg.brewery.web.controllers.BeerControllerIT#getStreamAllUsers") + void getBeerByIdAUTH(String user, String pwd) throws Exception{ + Beer beer = beerRepository.findAll().get(0); + + mockMvc.perform(get("/beers/" + beer.getId()) + .with(httpBasic(user, pwd))) + .andExpect(status().isOk()) + .andExpect(view().name("beers/beerDetails")) + .andExpect(model().attributeExists("beer")); + } + + @Test + void getBeerByIdNoAuth() throws Exception{ + Beer beer = beerRepository.findAll().get(0); + + mockMvc.perform(get("/beers/" + beer.getId())) + .andExpect(status().isUnauthorized()); + } + } +} \ No newline at end of file diff --git a/src/test/java/guru/sfg/brewery/web/controllers/BreweryControllerIT.java b/src/test/java/guru/sfg/brewery/web/controllers/BreweryControllerIT.java new file mode 100644 index 000000000..878a881ac --- /dev/null +++ b/src/test/java/guru/sfg/brewery/web/controllers/BreweryControllerIT.java @@ -0,0 +1,66 @@ +package guru.sfg.brewery.web.controllers; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +class BreweryControllerIT extends BaseIT { + + @Test + void listBreweriesCUSTOMER() throws Exception { + mockMvc.perform(get("/brewery/breweries") + .with(httpBasic("scott", "tiger"))) + .andExpect(status().is2xxSuccessful()); + } + + @Test + void listBreweriesADMIN() throws Exception { + mockMvc.perform(get("/brewery/breweries") + .with(httpBasic("spring", "guru"))) + .andExpect(status().is2xxSuccessful()); + } + + @Test + void listBreweriesUSER() throws Exception { + mockMvc.perform(get("/brewery/breweries") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + } + + @Test + void listBreweriesNOAUTH() throws Exception { + mockMvc.perform(get("/brewery/breweries")) + .andExpect(status().isUnauthorized()); + } + + @Test + void getBreweriesJsonCUSTOMER() throws Exception { + mockMvc.perform(get("/brewery/api/v1/breweries") + .with(httpBasic("scott", "tiger"))) + .andExpect(status().is2xxSuccessful()); + } + + @Test + void getBreweriesJsonADMIN() throws Exception { + mockMvc.perform(get("/brewery/api/v1/breweries") + .with(httpBasic("spring", "guru"))) + .andExpect(status().is2xxSuccessful()); + } + + @Test + void getBreweriesJsonUSER() throws Exception { + mockMvc.perform(get("/brewery/api/v1/breweries") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + } + + @Test + void getBreweriesJsonNOAUTH() throws Exception { + mockMvc.perform(get("/brewery/api/v1/breweries")) + .andExpect(status().isUnauthorized()); + } +} \ No newline at end of file diff --git a/src/test/java/guru/sfg/brewery/web/controllers/CustomerControllerIT.java b/src/test/java/guru/sfg/brewery/web/controllers/CustomerControllerIT.java new file mode 100644 index 000000000..57100313e --- /dev/null +++ b/src/test/java/guru/sfg/brewery/web/controllers/CustomerControllerIT.java @@ -0,0 +1,80 @@ +package guru.sfg.brewery.web.controllers; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.Rollback; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Created by jt on 6/27/20. + */ +@SpringBootTest +public class CustomerControllerIT extends BaseIT { + + @DisplayName("List Customers") + @Nested + class ListCustomers{ + @ParameterizedTest(name = "#{index} with [{arguments}]") + @MethodSource("guru.sfg.brewery.web.controllers.BeerControllerIT#getStreamAdminCustomer") + void testListCustomersAUTH(String user, String pwd) throws Exception { + mockMvc.perform(get("/customers") + .with(httpBasic(user, pwd))) + .andExpect(status().isOk()); + + } + + @Test + void testListCustomersNOTAUTH() throws Exception { + mockMvc.perform(get("/customers") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + } + + @Test + void testListCustomersNOTLOGGEDIN() throws Exception { + mockMvc.perform(get("/customers")) + .andExpect(status().isUnauthorized()); + + } + } + + @DisplayName("Add Customers") + @Nested + class AddCustomers { + + @Rollback + @Test + void processCreationForm() throws Exception{ + mockMvc.perform(post("/customers/new") + .param("customerName", "Foo Customer") + .with(httpBasic("spring", "guru"))) + .andExpect(status().is3xxRedirection()); + } + + @Rollback + @ParameterizedTest(name = "#{index} with [{arguments}]") + @MethodSource("guru.sfg.brewery.web.controllers.BeerControllerIT#getStreamNotAdmin") + void processCreationFormNOTAUTH(String user, String pwd) throws Exception{ + mockMvc.perform(post("/customers/new") + .param("customerName", "Foo Customer2") + .with(httpBasic(user, pwd))) + .andExpect(status().isForbidden()); + } + + @Test + void processCreationFormNOAUTH() throws Exception{ + mockMvc.perform(post("/customers/new") + .param("customerName", "Foo Customer")) + .andExpect(status().isUnauthorized()); + } + } + +} \ No newline at end of file diff --git a/src/test/java/guru/sfg/brewery/web/controllers/IndexControllerIT.java b/src/test/java/guru/sfg/brewery/web/controllers/IndexControllerIT.java new file mode 100644 index 000000000..34a60b439 --- /dev/null +++ b/src/test/java/guru/sfg/brewery/web/controllers/IndexControllerIT.java @@ -0,0 +1,41 @@ +package guru.sfg.brewery.web.controllers; + +import guru.sfg.brewery.repositories.BeerInventoryRepository; +import guru.sfg.brewery.repositories.BeerRepository; +import guru.sfg.brewery.repositories.CustomerRepository; +import guru.sfg.brewery.services.BeerService; +import guru.sfg.brewery.services.BreweryService; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Created by jt on 6/13/20. + */ +@WebMvcTest +public class IndexControllerIT extends BaseIT { + + @MockBean + BeerRepository beerRepository; + + @MockBean + BeerInventoryRepository beerInventoryRepository; + + @MockBean + BreweryService breweryService; + + @MockBean + CustomerRepository customerRepository; + + @MockBean + BeerService beerService; + + @Test + void testGetIndexSlash() throws Exception{ + mockMvc.perform(get("/" )) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/guru/sfg/brewery/web/controllers/PasswordEncodingTests.java b/src/test/java/guru/sfg/brewery/web/controllers/PasswordEncodingTests.java new file mode 100644 index 000000000..0bce4686b --- /dev/null +++ b/src/test/java/guru/sfg/brewery/web/controllers/PasswordEncodingTests.java @@ -0,0 +1,77 @@ +package guru.sfg.brewery.web.controllers; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.LdapShaPasswordEncoder; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.crypto.password.StandardPasswordEncoder; +import org.springframework.util.DigestUtils; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Created by jt on 6/16/20. + */ +@Disabled +public class PasswordEncodingTests { + + static final String PASSWORD = "password"; + + @Test + void testBcrypt15() { + PasswordEncoder bcrypt = new BCryptPasswordEncoder(15); + + System.out.println(bcrypt.encode(PASSWORD)); + System.out.println(bcrypt.encode(PASSWORD)); + System.out.println(bcrypt.encode("tiger")); + + } + + @Test + void testBcrypt() { + PasswordEncoder bcrypt = new BCryptPasswordEncoder(); + + System.out.println(bcrypt.encode(PASSWORD)); + System.out.println(bcrypt.encode(PASSWORD)); + System.out.println(bcrypt.encode("guru")); + + } + + @Test + void testSha256() { + PasswordEncoder sha256 = new StandardPasswordEncoder(); + + System.out.println(sha256.encode(PASSWORD)); + System.out.println(sha256.encode(PASSWORD)); + } + + @Test + void testLdap() { + PasswordEncoder ldap = new LdapShaPasswordEncoder(); + System.out.println(ldap.encode(PASSWORD)); + System.out.println(ldap.encode(PASSWORD)); + System.out.println(ldap.encode("tiger")); + String encodedPwd = ldap.encode(PASSWORD); + + assertTrue(ldap.matches(PASSWORD, encodedPwd )); + + } + + @Test + void testNoOp() { + PasswordEncoder noOp = NoOpPasswordEncoder.getInstance(); + + System.out.println(noOp.encode(PASSWORD)); + } + + @Test + void hashingExample() { + System.out.println(DigestUtils.md5DigestAsHex(PASSWORD.getBytes())); + System.out.println(DigestUtils.md5DigestAsHex(PASSWORD.getBytes())); + + String salted = PASSWORD + "ThisIsMySALTVALUE"; + System.out.println(DigestUtils.md5DigestAsHex(salted.getBytes())); + } +} diff --git a/src/test/java/guru/sfg/brewery/web/controllers/api/BeerOrderControllerTest.java b/src/test/java/guru/sfg/brewery/web/controllers/api/BeerOrderControllerTest.java new file mode 100644 index 000000000..0295a6daf --- /dev/null +++ b/src/test/java/guru/sfg/brewery/web/controllers/api/BeerOrderControllerTest.java @@ -0,0 +1,243 @@ +package guru.sfg.brewery.web.controllers.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import guru.sfg.brewery.bootstrap.DefaultBreweryLoader; +import guru.sfg.brewery.domain.Beer; +import guru.sfg.brewery.domain.BeerOrder; +import guru.sfg.brewery.domain.Customer; +import guru.sfg.brewery.repositories.BeerOrderRepository; +import guru.sfg.brewery.repositories.BeerRepository; +import guru.sfg.brewery.repositories.CustomerRepository; +import guru.sfg.brewery.web.controllers.BaseIT; +import guru.sfg.brewery.web.model.BeerOrderDto; +import guru.sfg.brewery.web.model.BeerOrderLineDto; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +class BeerOrderControllerTest extends BaseIT { + + public static final String API_ROOT = "/api/v1/customers/"; + + @Autowired + CustomerRepository customerRepository; + + @Autowired + BeerOrderRepository beerOrderRepository; + + @Autowired + BeerRepository beerRepository; + + @Autowired + ObjectMapper objectMapper; + + Customer stPeteCustomer; + Customer dunedinCustomer; + Customer keyWestCustomer; + List loadedBeers; + + @BeforeEach + void setUp() { + stPeteCustomer = customerRepository.findAllByCustomerName(DefaultBreweryLoader.ST_PETE_DISTRIBUTING).orElseThrow(); + dunedinCustomer = customerRepository.findAllByCustomerName(DefaultBreweryLoader.DUNEDIN_DISTRIBUTING).orElseThrow(); + keyWestCustomer = customerRepository.findAllByCustomerName(DefaultBreweryLoader.KEY_WEST_DISTRIBUTORS).orElseThrow(); + loadedBeers = beerRepository.findAll(); + } + +//cant use nested tests bug - https://github.com/spring-projects/spring-security/issues/8793 +// @DisplayName("Create Test") +// @Nested +// class createOrderTests { + + + @Test + void createOrderNotAuth() throws Exception { + BeerOrderDto beerOrderDto = buildOrderDto(stPeteCustomer, loadedBeers.get(0).getId()); + + mockMvc.perform(post(API_ROOT + stPeteCustomer.getId() + "/orders") + .accept(MediaType.APPLICATION_JSON) + .characterEncoding("UTF-8") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(beerOrderDto))) + .andExpect(status().isUnauthorized()); + } + + @WithUserDetails("spring") + @Test + void createOrderUserAdmin() throws Exception { + BeerOrderDto beerOrderDto = buildOrderDto(stPeteCustomer, loadedBeers.get(0).getId()); + + mockMvc.perform(post(API_ROOT + stPeteCustomer.getId() + "/orders") + .accept(MediaType.APPLICATION_JSON) + .characterEncoding("UTF-8") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(beerOrderDto))) + .andExpect(status().isCreated()); + } + + @WithUserDetails(DefaultBreweryLoader.STPETE_USER) + @Test + void createOrderUserAuthCustomer() throws Exception { + BeerOrderDto beerOrderDto = buildOrderDto(stPeteCustomer, loadedBeers.get(0).getId()); + + mockMvc.perform(post(API_ROOT + stPeteCustomer.getId() + "/orders") + .accept(MediaType.APPLICATION_JSON) + .characterEncoding("UTF-8") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(beerOrderDto))) + .andExpect(status().isCreated()); + } + + @WithUserDetails(DefaultBreweryLoader.KEYWEST_USER) + @Test + void createOrderUserNOTAuthCustomer() throws Exception { + BeerOrderDto beerOrderDto = buildOrderDto(stPeteCustomer, loadedBeers.get(0).getId()); + + mockMvc.perform(post(API_ROOT + stPeteCustomer.getId() + "/orders") + .accept(MediaType.APPLICATION_JSON) + .characterEncoding("UTF-8") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(beerOrderDto))) + .andExpect(status().isForbidden()); + } + + // } + @Test + void listOrdersNotAuth() throws Exception { + mockMvc.perform(get(API_ROOT + stPeteCustomer.getId() + "/orders")) + .andExpect(status().isUnauthorized()); + } + + @WithUserDetails(value = "spring") + @Test + void listOrdersAdminAuth() throws Exception { + mockMvc.perform(get(API_ROOT + stPeteCustomer.getId() + "/orders")) + .andExpect(status().isOk()); + } + + @WithUserDetails(value = DefaultBreweryLoader.STPETE_USER) + @Test + void listOrdersCustomerAuth() throws Exception { + mockMvc.perform(get(API_ROOT + stPeteCustomer.getId() + "/orders")) + .andExpect(status().isOk()); + } + + @WithUserDetails(value = DefaultBreweryLoader.DUNEDIN_USER) + @Test + void listOrdersCustomerNOTAuth() throws Exception { + mockMvc.perform(get(API_ROOT + stPeteCustomer.getId() + "/orders")) + .andExpect(status().isForbidden()); + } + + @Test + void listOrdersNoAuth() throws Exception { + mockMvc.perform(get(API_ROOT + stPeteCustomer.getId())) + .andExpect(status().isUnauthorized()); + } + + @Transactional + @Test + void getByOrderIdNotAuth() throws Exception { + BeerOrder beerOrder = stPeteCustomer.getBeerOrders().stream().findFirst().orElseThrow(); + + mockMvc.perform(get(API_ROOT + stPeteCustomer.getId() + "/orders/" + beerOrder.getId())) + .andExpect(status().isUnauthorized()); + } + + @Transactional + @WithUserDetails("spring") + @Test + void getByOrderIdADMIN() throws Exception { + BeerOrder beerOrder = stPeteCustomer.getBeerOrders().stream().findFirst().orElseThrow(); + + mockMvc.perform(get(API_ROOT + stPeteCustomer.getId() + "/orders/" + beerOrder.getId())) + .andExpect(status().is2xxSuccessful()); + } + + @Transactional + @WithUserDetails(DefaultBreweryLoader.STPETE_USER) + @Test + void getByOrderIdCustomerAuth() throws Exception { + BeerOrder beerOrder = stPeteCustomer.getBeerOrders().stream().findFirst().orElseThrow(); + + mockMvc.perform(get(API_ROOT + stPeteCustomer.getId() + "/orders/" + beerOrder.getId())) + .andExpect(status().is2xxSuccessful()); + } + + @Transactional + @WithUserDetails(DefaultBreweryLoader.DUNEDIN_USER) + @Test + void getByOrderIdCustomerNOTAuth() throws Exception { + BeerOrder beerOrder = stPeteCustomer.getBeerOrders().stream().findFirst().orElseThrow(); + + mockMvc.perform(get(API_ROOT + stPeteCustomer.getId() + "/orders/" + beerOrder.getId())) + .andExpect(status().isForbidden()); + } + + @Transactional + @Test + void pickUpOrderNotAuth() throws Exception { + BeerOrder beerOrder = stPeteCustomer.getBeerOrders().stream().findFirst().orElseThrow(); + + mockMvc.perform(put(API_ROOT + stPeteCustomer.getId() + "/orders/" + beerOrder.getId() + "/pickup")) + .andExpect(status().isUnauthorized()); + + } + + @Transactional + @WithUserDetails("spring") + @Test + void pickUpOrderAdminUser() throws Exception { + BeerOrder beerOrder = stPeteCustomer.getBeerOrders().stream().findFirst().orElseThrow(); + + mockMvc.perform(put(API_ROOT + stPeteCustomer.getId() + "/orders/" + beerOrder.getId() + "/pickup")) + .andExpect(status().isNoContent()); + } + + @Transactional + @WithUserDetails(DefaultBreweryLoader.STPETE_USER) + @Test + void pickUpOrderCustomerUserAUTH() throws Exception { + BeerOrder beerOrder = stPeteCustomer.getBeerOrders().stream().findFirst().orElseThrow(); + + mockMvc.perform(put(API_ROOT + stPeteCustomer.getId() + "/orders/" + beerOrder.getId() + "/pickup")) + .andExpect(status().isNoContent()); + } + + @Transactional + @WithUserDetails(DefaultBreweryLoader.DUNEDIN_USER) + @Test + void pickUpOrderCustomerUserNOT_AUTH() throws Exception { + BeerOrder beerOrder = stPeteCustomer.getBeerOrders().stream().findFirst().orElseThrow(); + + mockMvc.perform(put(API_ROOT + stPeteCustomer.getId() + "/orders/" + beerOrder.getId() + "/pickup")) + .andExpect(status().isForbidden()); + } + + private BeerOrderDto buildOrderDto(Customer customer, UUID beerId) { + List orderLines = Arrays.asList(BeerOrderLineDto.builder() + .id(UUID.randomUUID()) + .beerId(beerId) + .orderQuantity(5) + .build()); + + return BeerOrderDto.builder() + .customerId(customer.getId()) + .customerRef("123") + .orderStatusCallbackUrl("http://example.com") + .beerOrderLines(orderLines) + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/guru/sfg/brewery/web/controllers/api/BeerOrderControllerV2Test.java b/src/test/java/guru/sfg/brewery/web/controllers/api/BeerOrderControllerV2Test.java new file mode 100644 index 000000000..8df259754 --- /dev/null +++ b/src/test/java/guru/sfg/brewery/web/controllers/api/BeerOrderControllerV2Test.java @@ -0,0 +1,119 @@ +package guru.sfg.brewery.web.controllers.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import guru.sfg.brewery.bootstrap.DefaultBreweryLoader; +import guru.sfg.brewery.domain.Beer; +import guru.sfg.brewery.domain.BeerOrder; +import guru.sfg.brewery.domain.Customer; +import guru.sfg.brewery.repositories.BeerOrderRepository; +import guru.sfg.brewery.repositories.BeerRepository; +import guru.sfg.brewery.repositories.CustomerRepository; +import guru.sfg.brewery.web.controllers.BaseIT; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +class BeerOrderControllerV2Test extends BaseIT { + public static final String API_ROOT = "/api/v2/orders/"; + + @Autowired + CustomerRepository customerRepository; + + @Autowired + BeerOrderRepository beerOrderRepository; + + @Autowired + BeerRepository beerRepository; + + @Autowired + ObjectMapper objectMapper; + + Customer stPeteCustomer; + Customer dunedinCustomer; + Customer keyWestCustomer; + List loadedBeers; + + @BeforeEach + void setUp() { + stPeteCustomer = customerRepository.findAllByCustomerName(DefaultBreweryLoader.ST_PETE_DISTRIBUTING).orElseThrow(); + dunedinCustomer = customerRepository.findAllByCustomerName(DefaultBreweryLoader.DUNEDIN_DISTRIBUTING).orElseThrow(); + keyWestCustomer = customerRepository.findAllByCustomerName(DefaultBreweryLoader.KEY_WEST_DISTRIBUTORS).orElseThrow(); + loadedBeers = beerRepository.findAll(); + } + + @Test + void listOrdersNotAuth() throws Exception { + mockMvc.perform(get(API_ROOT)) + .andExpect(status().isUnauthorized()); + } + + @WithUserDetails(value = "spring") + @Test + void listOrdersAdminAuth() throws Exception { + mockMvc.perform(get(API_ROOT)) + .andExpect(status().isOk()); + } + + @WithUserDetails(value = DefaultBreweryLoader.STPETE_USER) + @Test + void listOrdersCustomerAuth() throws Exception { + mockMvc.perform(get(API_ROOT)) + .andExpect(status().isOk()); + } + + @WithUserDetails(value = DefaultBreweryLoader.DUNEDIN_USER) + @Test + void listOrdersCustomerDunedinAuth() throws Exception { + mockMvc.perform(get(API_ROOT )) + .andExpect(status().isOk()); + } + + + @Transactional + @Test + void getByOrderIdNotAuth() throws Exception { + BeerOrder beerOrder = stPeteCustomer.getBeerOrders().stream().findFirst().orElseThrow(); + + mockMvc.perform(get(API_ROOT + beerOrder.getId())) + .andExpect(status().isUnauthorized()); + } + + @Transactional + @WithUserDetails("spring") + @Test + void getByOrderIdADMIN() throws Exception { + BeerOrder beerOrder = stPeteCustomer.getBeerOrders().stream().findFirst().orElseThrow(); + + mockMvc.perform(get(API_ROOT + beerOrder.getId())) + .andExpect(status().is2xxSuccessful()); + } + + @Transactional + @WithUserDetails(DefaultBreweryLoader.STPETE_USER) + @Test + void getByOrderIdCustomerAuth() throws Exception { + BeerOrder beerOrder = stPeteCustomer.getBeerOrders().stream().findFirst().orElseThrow(); + + mockMvc.perform(get(API_ROOT + beerOrder.getId())) + .andExpect(status().is2xxSuccessful()); + } + + @Transactional + @WithUserDetails(DefaultBreweryLoader.DUNEDIN_USER) + @Test + void getByOrderIdCustomerNOTAuth() throws Exception { + BeerOrder beerOrder = stPeteCustomer.getBeerOrders().stream().findFirst().orElseThrow(); + + mockMvc.perform(get(API_ROOT + beerOrder.getId())) + .andExpect(status().isNotFound()); + } +} \ No newline at end of file diff --git a/src/test/java/guru/sfg/brewery/web/controllers/api/BeerRestControllerIT.java b/src/test/java/guru/sfg/brewery/web/controllers/api/BeerRestControllerIT.java new file mode 100644 index 000000000..b89d51f8d --- /dev/null +++ b/src/test/java/guru/sfg/brewery/web/controllers/api/BeerRestControllerIT.java @@ -0,0 +1,125 @@ +package guru.sfg.brewery.web.controllers.api; + +import guru.sfg.brewery.domain.Beer; +import guru.sfg.brewery.repositories.BeerRepository; +import guru.sfg.brewery.web.controllers.BaseIT; +import guru.sfg.brewery.web.model.BeerStyleEnum; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.Random; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Created by jt on 6/13/20. + */ +@SpringBootTest +public class BeerRestControllerIT extends BaseIT { + + @Autowired + BeerRepository beerRepository; + + @DisplayName("Delete Tests") + @Nested + class DeleteTests { + + public Beer beerToDelete() { + Random rand = new Random(); + + return beerRepository.saveAndFlush(Beer.builder() + .beerName("Delete Me Beer") + .beerStyle(BeerStyleEnum.IPA) + .minOnHand(12) + .quantityToBrew(200) + .upc(String.valueOf(rand.nextInt(99999999))) + .build()); + } + + @Test + void deleteBeerHttpBasic() throws Exception{ + mockMvc.perform(delete("/api/v1/beer/" + beerToDelete().getId()) + .with(httpBasic("spring", "guru"))) + .andExpect(status().is2xxSuccessful()); + } + + @ParameterizedTest(name = "#{index} with [{arguments}]") + @MethodSource("guru.sfg.brewery.web.controllers.BeerControllerIT#getStreamNotAdmin") + void deleteBeerHttpBasicNotAuth(String user, String pwd) throws Exception { + mockMvc.perform(delete("/api/v1/beer/" + beerToDelete().getId()) + .with(httpBasic(user, pwd))) + .andExpect(status().isForbidden()); + } + + @Test + void deleteBeerNoAuth() throws Exception { + mockMvc.perform(delete("/api/v1/beer/" + beerToDelete().getId())) + .andExpect(status().isUnauthorized()); + } + } + + @DisplayName("List Beers") + @Nested + class ListBeers { + @Test + void findBeers() throws Exception { + mockMvc.perform(get("/api/v1/beer/")) + .andExpect(status().isUnauthorized()); + } + + @ParameterizedTest(name = "#{index} with [{arguments}]") + @MethodSource("guru.sfg.brewery.web.controllers.BeerControllerIT#getStreamAllUsers") + void findBeersAUTH(String user, String pwd) throws Exception { + mockMvc.perform(get("/api/v1/beer/").with(httpBasic(user, pwd))) + .andExpect(status().isOk()); + } + } + + @DisplayName("Get Beer By ID") + @Nested + class GetBeerById { + @Test + void findBeerById() throws Exception { + Beer beer = beerRepository.findAll().get(0); + + mockMvc.perform(get("/api/v1/beer/" + beer.getId())) + .andExpect(status().isUnauthorized()); + } + + @ParameterizedTest(name = "#{index} with [{arguments}]") + @MethodSource("guru.sfg.brewery.web.controllers.BeerControllerIT#getStreamAllUsers") + void findBeerByIdAUTH(String user, String pwd) throws Exception { + Beer beer = beerRepository.findAll().get(0); + + mockMvc.perform(get("/api/v1/beer/" + beer.getId()) + .with(httpBasic(user, pwd))) + .andExpect(status().isOk()); + } + } + + @Nested + @DisplayName("Find By UPC") + class FindByUPC { + @Test + void findBeerByUpc() throws Exception { + mockMvc.perform(get("/api/v1/beerUpc/0631234200036")) + .andExpect(status().isUnauthorized()); + } + + @ParameterizedTest(name = "#{index} with [{arguments}]") + @MethodSource("guru.sfg.brewery.web.controllers.BeerControllerIT#getStreamAllUsers") + void findBeerByUpcAUTH(String user, String pwd) throws Exception { + mockMvc.perform(get("/api/v1/beerUpc/0631234200036") + .with(httpBasic(user, pwd))) + .andExpect(status().isOk()); + } + } +} \ No newline at end of file