Changed Authentication

This commit is contained in:
Samy Avrillon 2025-03-03 15:47:02 +01:00
parent 15f747807b
commit ab84002276
Signed by: Mysaa
GPG Key ID: 0220AC4A3D6A328B
15 changed files with 255 additions and 73 deletions

View File

@ -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<Privilege> inducedPrivileges = user.getRoles().stream()
.map(Role::getPrivileges)
.map(Set<Privilege>::stream)
.flatMap(Function.identity())
.sorted()
.distinct();
Stream<Role> 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<Role> roles) {
Collection < ? extends GrantedAuthority> mapRoles = roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
return mapRoles;
}
}

View File

@ -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")

View File

@ -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();
}
}

View File

@ -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<User> users;
@ElementCollection(fetch = FetchType.EAGER)
@Enumerated(EnumType.STRING)
@CollectionTable(name = "role_privileges"
, joinColumns = @JoinColumn(name = "id"))
@Column(name = "privileges", nullable = false)
private Set<Privilege> privileges;
public Role(String name){
super();
this.setName(name);
this.setPrivileges(EnumSet.noneOf(Privilege.class));
}
@Override
public String getAuthority() {
return "ROLE_"+name;
}
}

View File

@ -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)

View File

@ -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<ContextRefreshedEvent> {
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<Privilege> privileges) {
Role role = roleRepository.findByName(name);
if (role == null) {
role = new Role(name);
role.setPrivileges(privileges);
roleRepository.save(role);
}
return role;
}
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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<UserInfo> 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<UserInfo> 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<String> 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());
}
return new ResponseEntity<>(HttpStatus.OK);
@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<UserInfo> {
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);
}
}
}

View File

@ -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);

View File

@ -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;

View File

@ -74,3 +74,8 @@ div.welcome{
font-size: 20px;
font-style: italic;
}
table, th, td {
border: 1px solid black;
border-collapse: collapse;
padding: 4px;
}

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="fr" dir="ltr">
<head>
<div th:replace="~{html-head}"/>
</head>
<body>
<div th:replace="~{header}"/>
<main>
<div th:if="${param.success}">
<div class="alert alert-success"> Votre mot de passe a été changé</div><br/>
</div>
<form
method="post"
role="form"
th:action="@{/change-password/change}">
<label for="new-password">Nouveau mot de passe :</label>
<input type="password" id="new-password" name="new-password"/>
<br/>
<input id="connect" type="submit" value="Changer de mot de passe"/>
</form>
</main>
</body>
</html>

View File

@ -14,6 +14,9 @@
<div th:if="${param.logout}">
<div class="alert alert-success"> You have been logged out.</div><br/>
</div>
<div th:if="${param.restricted}">
<div class="alert alert-success"> Vous devez vous connecter pour vous accéder à cette page</div><br/>
</div>
<form
method="post"

View File

@ -9,19 +9,22 @@
<main>
<div th:if="${param.success}">
You have successfully registered our app!
Utilisteurice ajouté·e avec succès !
</div>
<div th:if="${param.duplicate}">
Le nom d'utilisateur·ice est déjà utilisé
</div>
<form
method="post"
role="form"
th:action="@{/adduser/save}"
th:object="${user}"
th:action="@{/adduser}"
th:object="${newuser}"
>
<label for="name">Name</label>
<label for="name">Nom</label>
<input
id="name"
name="name"
placeholder="Enter pseudo"
placeholder="Le pseudo"
th:field="*{name}"
type="text"
/>
@ -40,6 +43,19 @@
<button class="btn btn-primary" type="submit">Register</button>
</form>
<table>
<tr>
<th>ID</th>
<th>Pseudo</th>
<th>Roles</th>
</tr>
<tr th:each="u : ${users}">
<td th:text="${u.id}"></td>
<td th:text="${u.pseudo}"></td>
<td th:text="${u.roles}"></td>
</tr>
</table>
</main>
</body>
</html>