diff --git a/src/main/java/com/bernard/misael/CustomUserDetailsService.java b/src/main/java/com/bernard/misael/CustomUserDetailsService.java index 810043c..e818674 100644 --- a/src/main/java/com/bernard/misael/CustomUserDetailsService.java +++ b/src/main/java/com/bernard/misael/CustomUserDetailsService.java @@ -7,12 +7,17 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +import com.bernard.misael.model.Privilege; import com.bernard.misael.model.Role; import com.bernard.misael.model.User; import com.bernard.misael.repository.UserRepository; import java.util.Collection; +import java.util.EnumSet; +import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; @Service public class CustomUserDetailsService implements UserDetailsService { @@ -28,18 +33,19 @@ public class CustomUserDetailsService implements UserDetailsService { User user = userRepository.findByName(pseudo); if (user != null) { + Stream inducedPrivileges = user.getRoles().stream() + .map(Role::getPrivileges) + .map(Set::stream) + .flatMap(Function.identity()) + .sorted() + .distinct(); + Stream roles = user.getRoles().stream(); return new org.springframework.security.core.userdetails.User(user.getName(), user.getPassword(), - mapRolesToAuthorities(user.getRoles())); + Stream.concat(inducedPrivileges, roles).toList() + ); }else{ throw new UsernameNotFoundException("Invalid username or password."); } } - - private Collection < ? extends GrantedAuthority> mapRolesToAuthorities(Collection roles) { - Collection < ? extends GrantedAuthority> mapRoles = roles.stream() - .map(role -> new SimpleGrantedAuthority(role.getName())) - .collect(Collectors.toList()); - return mapRoles; - } } \ No newline at end of file diff --git a/src/main/java/com/bernard/misael/SpringSecurity.java b/src/main/java/com/bernard/misael/SpringSecurity.java index fb1b0f5..f3cfb28 100644 --- a/src/main/java/com/bernard/misael/SpringSecurity.java +++ b/src/main/java/com/bernard/misael/SpringSecurity.java @@ -1,34 +1,24 @@ package com.bernard.misael; -import java.security.Principal; -import java.util.List; -import java.util.Random; -import java.util.UUID; import java.util.logging.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import com.bernard.misael.model.User; -import com.bernard.misael.service.UserService; - -import jakarta.annotation.PostConstruct; @Configuration @EnableWebSecurity +@EnableMethodSecurity(securedEnabled = true) public class SpringSecurity { public static final Logger LOG = Logger.getLogger(SpringSecurity.class.getName()); @@ -43,11 +33,10 @@ public class SpringSecurity { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http.csrf().disable() + http.csrf(csrf -> csrf.disable()) .authorizeHttpRequests((authorize) -> - authorize.requestMatchers("/**").permitAll() - // .requestMatchers("/index").permitAll() - // .requestMatchers("/users").hasRole("ADMIN") + authorize + .requestMatchers("/**").permitAll() ).formLogin( form -> form .loginPage("/login") diff --git a/src/main/java/com/bernard/misael/model/Privilege.java b/src/main/java/com/bernard/misael/model/Privilege.java new file mode 100644 index 0000000..8f36940 --- /dev/null +++ b/src/main/java/com/bernard/misael/model/Privilege.java @@ -0,0 +1,14 @@ +package com.bernard.misael.model; + +import org.springframework.security.core.GrantedAuthority; + +public enum Privilege implements GrantedAuthority { + + LIST_USERS,ADD_USERS,LIST_QUIZZ; + + @Override + public String getAuthority() { + return this.name(); + } + +} diff --git a/src/main/java/com/bernard/misael/model/Role.java b/src/main/java/com/bernard/misael/model/Role.java index bbe7cb7..cca024d 100644 --- a/src/main/java/com/bernard/misael/model/Role.java +++ b/src/main/java/com/bernard/misael/model/Role.java @@ -6,7 +6,12 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.Collection; +import java.util.EnumSet; import java.util.List; +import java.util.Set; + +import org.springframework.security.core.GrantedAuthority; @Getter @Setter @@ -14,7 +19,7 @@ import java.util.List; @AllArgsConstructor @Entity @Table(name="roles") -public class Role +public class Role implements GrantedAuthority { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -25,4 +30,22 @@ public class Role @ManyToMany(mappedBy="roles") private List users; + + @ElementCollection(fetch = FetchType.EAGER) + @Enumerated(EnumType.STRING) + @CollectionTable(name = "role_privileges" + , joinColumns = @JoinColumn(name = "id")) + @Column(name = "privileges", nullable = false) + private Set privileges; + + public Role(String name){ + super(); + this.setName(name); + this.setPrivileges(EnumSet.noneOf(Privilege.class)); + } + + @Override + public String getAuthority() { + return "ROLE_"+name; + } } diff --git a/src/main/java/com/bernard/misael/model/User.java b/src/main/java/com/bernard/misael/model/User.java index d074884..4cff658 100644 --- a/src/main/java/com/bernard/misael/model/User.java +++ b/src/main/java/com/bernard/misael/model/User.java @@ -26,7 +26,7 @@ public class User @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable=false) + @Column(nullable=false,unique = true) private String name; @Column(nullable=false) diff --git a/src/main/java/com/bernard/misael/service/AdminMaker.java b/src/main/java/com/bernard/misael/service/AdminMaker.java new file mode 100644 index 0000000..b1e28fb --- /dev/null +++ b/src/main/java/com/bernard/misael/service/AdminMaker.java @@ -0,0 +1,74 @@ +package com.bernard.misael.service; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.logging.Logger; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.lang.NonNull; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import com.bernard.misael.model.Privilege; +import com.bernard.misael.model.Role; +import com.bernard.misael.model.User; +import com.bernard.misael.repository.RoleRepository; +import com.bernard.misael.repository.UserRepository; + +import jakarta.transaction.Transactional; + +@Component +public class AdminMaker implements + ApplicationListener { + + boolean alreadySetup = false; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + @Transactional + public void onApplicationEvent(@NonNull ContextRefreshedEvent event) { + + if (alreadySetup) + return; + Logger log = Logger.getLogger("AdminMaker"); + log.info("Checking that privileges and mysaa user exist"); + + Role adminRole = createRoleIfNotFound("ADMIN", EnumSet.allOf(Privilege.class)); + createRoleIfNotFound("USER", EnumSet.noneOf(Privilege.class)); + + User mysaa = userRepository.findByName("mysaa"); + if (mysaa == null) { + User user = new User(); + user.setName("mysaa"); + user.setPassword(passwordEncoder.encode("super")); + user.setRoles(Arrays.asList(adminRole)); + userRepository.save(user); + } + + alreadySetup = true; + log.info("Everything needed has been created"); + } + + @Transactional + private Role createRoleIfNotFound( + String name, EnumSet privileges) { + + Role role = roleRepository.findByName(name); + if (role == null) { + role = new Role(name); + role.setPrivileges(privileges); + roleRepository.save(role); + } + return role; + } +} diff --git a/src/main/java/com/bernard/misael/service/UserService.java b/src/main/java/com/bernard/misael/service/UserService.java index 127b2b3..4969a7a 100644 --- a/src/main/java/com/bernard/misael/service/UserService.java +++ b/src/main/java/com/bernard/misael/service/UserService.java @@ -6,7 +6,9 @@ import com.bernard.misael.model.User; import com.bernard.misael.service.dto.UserDto; public interface UserService { + void saveUser(UserDto userDto); + void changePassword(User user, String password); User findUserByName(String name); diff --git a/src/main/java/com/bernard/misael/service/UserServiceImpl.java b/src/main/java/com/bernard/misael/service/UserServiceImpl.java index 75e49d3..cd4703c 100644 --- a/src/main/java/com/bernard/misael/service/UserServiceImpl.java +++ b/src/main/java/com/bernard/misael/service/UserServiceImpl.java @@ -34,13 +34,13 @@ public class UserServiceImpl implements UserService { // encrypt the password using spring security user.setPassword(passwordEncoder.encode(userDto.getPassword())); - Role role = roleRepository.findByName("ROLE_ADMIN"); - if(role == null){ - role = checkRoleExist(); - } - user.setRoles(Arrays.asList(role)); + user.setRoles(List.of()); userRepository.save(user); } + @Override + public void changePassword(User user, String password) { + user.setPassword(passwordEncoder.encode(password)); + } @Override public User findUserByName(String name) { @@ -60,10 +60,4 @@ public class UserServiceImpl implements UserService { userDto.setName(user.getName()); return userDto; } - - private Role checkRoleExist(){ - Role role = new Role(); - role.setName("ROLE_ADMIN"); - return roleRepository.save(role); - } } \ No newline at end of file diff --git a/src/main/java/com/bernard/misael/web/AuthController.java b/src/main/java/com/bernard/misael/web/AuthController.java index ea899fc..9bd0e29 100644 --- a/src/main/java/com/bernard/misael/web/AuthController.java +++ b/src/main/java/com/bernard/misael/web/AuthController.java @@ -1,14 +1,11 @@ package com.bernard.misael.web; +import java.security.Principal; import java.util.List; -import java.util.Random; -import java.util.UUID; -import java.util.logging.Logger; +import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.HttpStatusCode; -import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -16,18 +13,24 @@ import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import com.bernard.misael.model.Role; import com.bernard.misael.model.User; +import com.bernard.misael.repository.UserRepository; import com.bernard.misael.service.UserService; import com.bernard.misael.service.dto.UserDto; import jakarta.validation.Valid; +import lombok.Getter; @Controller public class AuthController { @Autowired private UserService userService; + @Autowired + private UserRepository urepo; public AuthController(UserService userService) { this.userService = userService; @@ -56,44 +59,68 @@ public class AuthController { return "index"; } - @GetMapping("/adduser") - public String showRegistrationForm(Model model){ - // create model object to store form data - UserDto user = new UserDto(); - model.addAttribute("user", user); - return "adduser"; + @GetMapping("/users") + @Secured("LIST_USERS") + public String listUsers(Model model, Principal p){ + // User should have MANAGE_USERS now + UserDto newUser = new UserDto(); + model.addAttribute("newuser",newUser); + List userz = urepo.findAll().stream().map(UserInfo::new).sorted().toList(); + model.addAttribute("users", userz); + return "users"; } - @PostMapping("/adduser/save") - public String registration(@Valid @ModelAttribute("user") UserDto userDto, + @PostMapping("/adduser") + @Secured("ADD_USERS") + public String registration(@Valid @ModelAttribute("newuser") UserDto userDto, BindingResult result, - Model model){ - User existingUser = userService.findUserByName("mysaa"); + Model model, + Principal p){ + User existingUser = userService.findUserByName(userDto.getName()); if(existingUser != null){ result.reject("User is already registered"); } if(result.hasErrors()){ - model.addAttribute("user", userDto); - return "/adduser"; + List userz = urepo.findAll().stream().map(UserInfo::new).sorted().toList(); + model.addAttribute("users", userz); + model.addAttribute("newuser",userDto); + return "redirect:/users?duplicate"; } userService.saveUser(userDto); - return "redirect:/adduser?success"; + return "redirect:/users?success"; } - @GetMapping("/create-mysaa-user") - public ResponseEntity checkMysaaExists() { - if(userService.findUserByName("mysaa") == null) { - UserDto u = new UserDto(); - Random r = new Random(); - UUID pass = new UUID(r.nextLong(),r.nextLong()); - u.setName("mysaa"); - u.setPassword(pass.toString()); - userService.saveUser(u); - Logger.getLogger(AuthController.class.getName()).warning("Created user mysaa with password "+pass.toString()); + @GetMapping("/change-password") + public String showChangePassword(){ + return "change-password"; + } + + @PostMapping("/change-password/change") + public String changePassword(@RequestParam("new-password") String newPassword, Principal p) { + User u = null; + if (p==null) + return "redirect:/login?restricted"; + u = userService.findUserByName(p.getName()); + userService.changePassword(u, newPassword); + + return "redirect:/change-password?success"; + } + + @Getter + public static class UserInfo implements Comparable { + private long id; + private String pseudo; + private String roles; + public UserInfo(User u){ + this.id = u.getId(); + this.pseudo = u.getName(); + this.roles = u.getRoles().stream().map(Role::getName).collect(Collectors.joining(";")); + } + @Override + public int compareTo(UserInfo other) { + return this.pseudo.compareTo(other.pseudo); } - return new ResponseEntity<>(HttpStatus.OK); } - } diff --git a/src/main/java/com/bernard/misael/web/QuestionsController.java b/src/main/java/com/bernard/misael/web/QuestionsController.java index 43bbbc0..c730c95 100644 --- a/src/main/java/com/bernard/misael/web/QuestionsController.java +++ b/src/main/java/com/bernard/misael/web/QuestionsController.java @@ -66,6 +66,8 @@ public class QuestionsController { User u = null; if (p!=null) u = ur.findByName(p.getName()); + else + return "redirect:/login?restricted"; //XXX test that user can answer quizz m.addAttribute("formid", quizzId); Quizz q = qrepo.getById(quizzId); diff --git a/src/main/resources/db/migration/V3__added_role_privileges.sql b/src/main/resources/db/migration/V3__added_role_privileges.sql new file mode 100644 index 0000000..78ca569 --- /dev/null +++ b/src/main/resources/db/migration/V3__added_role_privileges.sql @@ -0,0 +1,2 @@ +create table role_privileges (id bigint not null, privileges varchar(255) not null check (privileges in ('LIST_USERS','ADD_USERS','LIST_QUIZZ')), primary key (id, privileges)); +alter table if exists role_privileges add constraint FK7xfa4foqs58j7rhk6ex78hpf3 foreign key (id) references roles; \ No newline at end of file diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css index 658a11f..a12918b 100644 --- a/src/main/resources/static/css/style.css +++ b/src/main/resources/static/css/style.css @@ -73,4 +73,9 @@ div.welcome{ color: #fff; font-size: 20px; font-style: italic; +} +table, th, td { + border: 1px solid black; + border-collapse: collapse; + padding: 4px; } \ No newline at end of file diff --git a/src/main/resources/templates/change-password.html b/src/main/resources/templates/change-password.html new file mode 100644 index 0000000..0ba96b4 --- /dev/null +++ b/src/main/resources/templates/change-password.html @@ -0,0 +1,25 @@ + + + +
+ + + +
+
+ +
+
Votre mot de passe a été changé

+
+
+ + +
+ +
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index a648950..3539710 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -14,6 +14,9 @@
You have been logged out.

+
+
Vous devez vous connecter pour vous accéder à cette page

+
- You have successfully registered our app! + Utilisteurice ajouté·e avec succès ! +
+
+ Le nom d'utilisateur·ice est déjà utilisé
- + @@ -40,6 +43,19 @@
+ + + + + + + + + + + + +
IDPseudoRoles
\ No newline at end of file