use spring security more

This commit is contained in:
Stepan Usatiuk
2023-12-30 14:02:58 +01:00
parent d31ded94b2
commit d83bed9edd
22 changed files with 177 additions and 135 deletions

View File

@@ -6,6 +6,7 @@ import jakarta.validation.ConstraintViolationException;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ControllerAdvice;
@@ -33,6 +34,13 @@ public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
new HttpHeaders(), HttpStatus.UNAUTHORIZED, request); new HttpHeaders(), HttpStatus.UNAUTHORIZED, request);
} }
@ExceptionHandler(AccessDeniedException.class)
protected ResponseEntity<Object> handleAccessDeniedException(AccessDeniedException ex, WebRequest request) {
return handleExceptionInternal(ex,
new ErrorTo(List.of(ex.getMessage()), HttpStatus.FORBIDDEN.value()),
new HttpHeaders(), HttpStatus.FORBIDDEN, request);
}
@ExceptionHandler(UsernameNotFoundException.class) @ExceptionHandler(UsernameNotFoundException.class)
protected ResponseEntity<Object> handleUsernameNotFoundException(UsernameNotFoundException ex, WebRequest request) { protected ResponseEntity<Object> handleUsernameNotFoundException(UsernameNotFoundException ex, WebRequest request) {
return handleExceptionInternal(ex, return handleExceptionInternal(ex,

View File

@@ -26,29 +26,29 @@ public class ChatController {
} }
@GetMapping(path = "/by-id/{id}") @GetMapping(path = "/by-id/{id}")
public ChatTo get(Authentication authentication, @PathVariable Long id) { public ChatTo get(@PathVariable Long id) {
return chatService.getById(authentication, id); return chatService.getById(id);
} }
@DeleteMapping(path = "/by-id/{id}") @DeleteMapping(path = "/by-id/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) @ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(Authentication authentication, @PathVariable Long id) { public void delete(@PathVariable Long id) {
chatService.deleteById(authentication, id); chatService.deleteById(id);
} }
@PatchMapping(path = "/by-id/{id}") @PatchMapping(path = "/by-id/{id}")
public ChatTo update(Authentication authentication, @PathVariable Long id, @RequestBody ChatCreateTo chatCreateTo) { public ChatTo update(Authentication authentication, @PathVariable Long id, @RequestBody ChatCreateTo chatCreateTo) {
return chatService.update(authentication, id, chatCreateTo); return chatService.update(authentication, id, chatCreateTo);
} }
@GetMapping(path = "/my") @GetMapping(path = "/my")
public Collection<ChatTo> getMy(Authentication authentication) { public Collection<ChatTo> getMy(Authentication authentication) {
return chatService.getMy(authentication); return chatService.getMy(authentication);
} }
@GetMapping(path = "/by-id/{id}/members") @GetMapping(path = "/by-id/{id}/members")
public Collection<PersonTo> getMembers(Authentication authentication, @PathVariable Long id) { public Collection<PersonTo> getMembers(@PathVariable Long id) {
return chatService.getMembers(authentication, id); return chatService.getMembers(id);
} }

View File

@@ -21,7 +21,7 @@ public class MessageController {
@GetMapping(path = "/by-chat/{chatTd}") @GetMapping(path = "/by-chat/{chatTd}")
public Collection<MessageTo> get(Authentication authentication, @PathVariable Long chatTd) { public Collection<MessageTo> get(Authentication authentication, @PathVariable Long chatTd) {
return messageService.getByChat(authentication, chatTd); return messageService.getByChat(chatTd);
} }
@PostMapping(path = "/by-chat/{chatId}") @PostMapping(path = "/by-chat/{chatId}")
@@ -30,19 +30,19 @@ public class MessageController {
} }
@PatchMapping(path = "/by-id/{id}") @PatchMapping(path = "/by-id/{id}")
public MessageTo update(Authentication authentication, @PathVariable long id, @RequestBody MessageCreateTo messageCreateTo) { public MessageTo update(@PathVariable long id, @RequestBody MessageCreateTo messageCreateTo) {
return messageService.update(authentication, id, messageCreateTo); return messageService.update(id, messageCreateTo);
} }
@DeleteMapping(path = "/by-id/{id}") @DeleteMapping(path = "/by-id/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) @ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(Authentication authentication, @PathVariable long id) { public void delete(@PathVariable long id) {
messageService.delete(authentication, id); messageService.delete(id);
} }
@GetMapping @GetMapping
public Collection<MessageTo> getAll(Authentication authentication) { public Collection<MessageTo> getAll() {
return messageService.readAll(authentication); return messageService.readAll();
} }

View File

@@ -54,8 +54,8 @@ public class PersonController {
@DeleteMapping(path = "/by-uuid/{uuid}") @DeleteMapping(path = "/by-uuid/{uuid}")
@ResponseStatus(HttpStatus.NO_CONTENT) @ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteByUuid(Authentication authentication, @PathVariable String uuid) throws UserNotFoundException { public void deleteByUuid(@PathVariable String uuid) throws UserNotFoundException {
personService.deleteByUuid(authentication, uuid); personService.deleteByUuid(uuid);
} }
@@ -81,14 +81,14 @@ public class PersonController {
@PutMapping(path = "/admins/{uuid}") @PutMapping(path = "/admins/{uuid}")
@ResponseStatus(HttpStatus.NO_CONTENT) @ResponseStatus(HttpStatus.NO_CONTENT)
public void addAdmin(Authentication authentication, @PathVariable String uuid) throws UserNotFoundException { public void addAdmin(@PathVariable String uuid) throws UserNotFoundException {
personService.addAdmin(authentication, uuid); personService.addAdmin(uuid);
} }
@DeleteMapping(path = "/admins/{uuid}") @DeleteMapping(path = "/admins/{uuid}")
@ResponseStatus(HttpStatus.NO_CONTENT) @ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteAdmin(Authentication authentication, @PathVariable String uuid) throws UserNotFoundException { public void deleteAdmin(@PathVariable String uuid) throws UserNotFoundException {
personService.removeAdmin(authentication, uuid); personService.removeAdmin(uuid);
} }
@PutMapping(path = "/following/{uuid}") @PutMapping(path = "/following/{uuid}")

View File

@@ -47,19 +47,19 @@ public class PostController {
} }
@PatchMapping(path = "/{id}") @PatchMapping(path = "/{id}")
public PostTo update(Authentication authentication, @PathVariable long id, @RequestBody PostCreateTo postCreateTo) { public PostTo update(@PathVariable long id, @RequestBody PostCreateTo postCreateTo) {
return postService.updatePost(authentication, id, postCreateTo); return postService.updatePost(id, postCreateTo);
} }
@DeleteMapping(path = "/{id}") @DeleteMapping(path = "/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) @ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(Authentication authentication, @PathVariable long id) { public void delete(@PathVariable long id) {
postService.deletePost(authentication, id); postService.deletePost(id);
} }
@GetMapping @GetMapping
public Collection<PostTo> getAll(Authentication authentication) { public Collection<PostTo> getAll() {
return postService.readAll(authentication); return postService.readAll();
} }
} }

View File

@@ -17,7 +17,7 @@ import java.util.Collection;
@NoArgsConstructor @NoArgsConstructor
@ToString @ToString
@Accessors(chain = true) @Accessors(chain = true)
public class Chat implements EntityWithId<Long> { public class Chat {
@Id @Id
@GeneratedValue @GeneratedValue
private Long id; private Long id;

View File

@@ -1,5 +0,0 @@
package com.usatiuk.tjv.y.server.entity;
public interface EntityWithId<ID> {
ID getId();
}

View File

@@ -17,7 +17,7 @@ import java.time.Instant;
@NoArgsConstructor @NoArgsConstructor
@ToString @ToString
@Accessors(chain = true) @Accessors(chain = true)
public class Message implements EntityWithId<Long> { public class Message {
@Id @Id
@GeneratedValue @GeneratedValue
private Long id; private Long id;

View File

@@ -18,7 +18,7 @@ import java.util.Collection;
@NoArgsConstructor @NoArgsConstructor
@ToString @ToString
@Accessors(chain = true) @Accessors(chain = true)
public class Person implements EntityWithId<String> { public class Person {
@Id @Id
@GeneratedValue(strategy = GenerationType.UUID) @GeneratedValue(strategy = GenerationType.UUID)
private String uuid; private String uuid;
@@ -57,8 +57,4 @@ public class Person implements EntityWithId<String> {
@ManyToMany(mappedBy = "members") @ManyToMany(mappedBy = "members")
private Collection<Chat> chats; private Collection<Chat> chats;
@Override
public String getId() {
return uuid;
}
} }

View File

@@ -17,7 +17,7 @@ import java.time.Instant;
@NoArgsConstructor @NoArgsConstructor
@ToString @ToString
@Accessors(chain = true) @Accessors(chain = true)
public class Post implements EntityWithId<Long> { public class Post {
@Id @Id
@GeneratedValue @GeneratedValue
private Long id; private Long id;
@@ -31,9 +31,4 @@ public class Post implements EntityWithId<Long> {
@CreationTimestamp @CreationTimestamp
private Instant createdAt; private Instant createdAt;
@Override
public Long getId() {
return id;
}
} }

View File

@@ -10,6 +10,7 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer; import org.springframework.security.config.Customizer;
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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
@@ -31,6 +32,7 @@ import java.util.List;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig { public class WebSecurityConfig {
private final JwtRequestFilter jwtRequestFilter; private final JwtRequestFilter jwtRequestFilter;

View File

@@ -3,20 +3,27 @@ package com.usatiuk.tjv.y.server.service;
import com.usatiuk.tjv.y.server.dto.ChatCreateTo; import com.usatiuk.tjv.y.server.dto.ChatCreateTo;
import com.usatiuk.tjv.y.server.dto.ChatTo; import com.usatiuk.tjv.y.server.dto.ChatTo;
import com.usatiuk.tjv.y.server.dto.PersonTo; import com.usatiuk.tjv.y.server.dto.PersonTo;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import java.util.Collection; import java.util.Collection;
public interface ChatService { public interface ChatService {
ChatTo create(Authentication authentication, ChatCreateTo chatCreateTo); ChatTo create(Authentication authentication, ChatCreateTo chatCreateTo);
@PreAuthorize("@chatService.isCreatorOf(authentication.principal.username, #id)")
ChatTo update(Authentication authentication, Long id, ChatCreateTo chatCreateTo); ChatTo update(Authentication authentication, Long id, ChatCreateTo chatCreateTo);
Collection<ChatTo> getMy(Authentication authentication); Collection<ChatTo> getMy(Authentication authentication);
ChatTo getById(Authentication authentication, Long id); @PreAuthorize("@chatService.isMemberOf(authentication.principal.username, #id)")
void deleteById(Authentication authentication, Long id); ChatTo getById(Long id);
@PreAuthorize("@chatService.isMemberOf(authentication.principal.username, #id)")
void deleteById(Long id);
Collection<PersonTo> getMembers(Authentication authentication, Long id); @PreAuthorize("@chatService.isMemberOf(authentication.principal.username, #id)")
Collection<PersonTo> getMembers(Long id);
boolean isMemberOf(String personUuid, Long chatId); boolean isMemberOf(String personUuid, Long chatId);
boolean isCreatorOf(String personUuid, Long chatId);
} }

View File

@@ -19,7 +19,7 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Objects; import java.util.Objects;
@Service @Service("chatService")
public class ChatServiceImpl implements ChatService { public class ChatServiceImpl implements ChatService {
private final ChatRepository chatRepository; private final ChatRepository chatRepository;
@@ -57,8 +57,6 @@ public class ChatServiceImpl implements ChatService {
@Override @Override
public ChatTo update(Authentication authentication, Long id, ChatCreateTo chatCreateTo) { public ChatTo update(Authentication authentication, Long id, ChatCreateTo chatCreateTo) {
var chat = chatRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found")); var chat = chatRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found"));
if (!Objects.equals(chat.getCreator().getUuid(), authentication.getName()))
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "User isn't creator of the chat");
if (Arrays.stream(chatCreateTo.memberUuids()).noneMatch(n -> Objects.equals(n, authentication.getName()))) if (Arrays.stream(chatCreateTo.memberUuids()).noneMatch(n -> Objects.equals(n, authentication.getName())))
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Creator of chat must be its member"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Creator of chat must be its member");
@@ -81,29 +79,20 @@ public class ChatServiceImpl implements ChatService {
} }
@Override @Override
public ChatTo getById(Authentication authentication, Long id) { public ChatTo getById(Long id) {
var chat = chatRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found")); var chat = chatRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found"));
var userRef = entityManager.getReference(Person.class, authentication.getName());
if (!chat.getMembers().contains(userRef))
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "User isn't member of the chat");
return chatMapper.makeDto(chat); return chatMapper.makeDto(chat);
} }
@Override @Override
public void deleteById(Authentication authentication, Long id) { public void deleteById(Long id) {
var chat = chatRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found")); var chat = chatRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found"));
if (!Objects.equals(chat.getCreator().getUuid(), authentication.getName()))
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "User isn't creator of the chat");
chatRepository.delete(chat); chatRepository.delete(chat);
} }
@Override @Override
public Collection<PersonTo> getMembers(Authentication authentication, Long id) { public Collection<PersonTo> getMembers(Long id) {
var chat = chatRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found")); var chat = chatRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found"));
var userRef = entityManager.getReference(Person.class, authentication.getName());
if (!chat.getMembers().contains(userRef))
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "User isn't member of the chat");
return chat.getMembers().stream().map(personMapper::makeDto).toList(); return chat.getMembers().stream().map(personMapper::makeDto).toList();
} }
@@ -113,4 +102,10 @@ public class ChatServiceImpl implements ChatService {
var userRef = entityManager.getReference(Person.class, personUuid); var userRef = entityManager.getReference(Person.class, personUuid);
return chat.getMembers().contains(userRef); return chat.getMembers().contains(userRef);
} }
@Override
public boolean isCreatorOf(String personUuid, Long chatId) {
var chat = chatRepository.findById(chatId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found"));
return Objects.equals(chat.getCreator().getUuid(), personUuid);
}
} }

View File

@@ -2,16 +2,25 @@ package com.usatiuk.tjv.y.server.service;
import com.usatiuk.tjv.y.server.dto.MessageCreateTo; import com.usatiuk.tjv.y.server.dto.MessageCreateTo;
import com.usatiuk.tjv.y.server.dto.MessageTo; import com.usatiuk.tjv.y.server.dto.MessageTo;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import java.util.Collection; import java.util.Collection;
public interface MessageService { public interface MessageService {
Collection<MessageTo> getByChat(Authentication authentication, Long chatId); @PreAuthorize("@chatService.isMemberOf(authentication.principal.username, #chatId)")
Collection<MessageTo> getByChat(Long chatId);
@PreAuthorize("@chatService.isMemberOf(authentication.principal.username, #chatId)")
MessageTo addToChat(Authentication authentication, Long chatId, MessageCreateTo messageCreateTo); MessageTo addToChat(Authentication authentication, Long chatId, MessageCreateTo messageCreateTo);
MessageTo update(Authentication authentication, Long id, MessageCreateTo msg); @PreAuthorize("@messageService.isAuthorOf(authentication.principal.username, #id)")
void delete(Authentication authentication, Long id); MessageTo update(Long id, MessageCreateTo msg);
@PreAuthorize("@messageService.isAuthorOf(authentication.principal.username, #id)")
void delete(Long id);
Collection<MessageTo> readAll(Authentication authentication); @Secured({"ROLE_ADMIN"})
Collection<MessageTo> readAll();
boolean isAuthorOf(String userUuid, Long messageId);
} }

View File

@@ -7,11 +7,9 @@ import com.usatiuk.tjv.y.server.entity.Chat;
import com.usatiuk.tjv.y.server.entity.Message; import com.usatiuk.tjv.y.server.entity.Message;
import com.usatiuk.tjv.y.server.entity.Person; import com.usatiuk.tjv.y.server.entity.Person;
import com.usatiuk.tjv.y.server.repository.MessageRepository; import com.usatiuk.tjv.y.server.repository.MessageRepository;
import com.usatiuk.tjv.y.server.security.UserRoles;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
@@ -19,34 +17,26 @@ import java.util.Collection;
import java.util.Objects; import java.util.Objects;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
@Service @Service("messageService")
public class MessageServiceImpl implements MessageService { public class MessageServiceImpl implements MessageService {
private final MessageRepository messageRepository; private final MessageRepository messageRepository;
private final MessageMapper messageMapper; private final MessageMapper messageMapper;
private final ChatService chatService;
private final EntityManager entityManager; private final EntityManager entityManager;
public MessageServiceImpl(MessageRepository messageRepository, MessageMapper messageMapper, ChatService chatService, EntityManager entityManager) { public MessageServiceImpl(MessageRepository messageRepository, MessageMapper messageMapper, EntityManager entityManager) {
this.messageRepository = messageRepository; this.messageRepository = messageRepository;
this.messageMapper = messageMapper; this.messageMapper = messageMapper;
this.chatService = chatService;
this.entityManager = entityManager; this.entityManager = entityManager;
} }
@Override @Override
public Collection<MessageTo> getByChat(Authentication authentication, Long chatId) { public Collection<MessageTo> getByChat(Long chatId) {
if (!chatService.isMemberOf(authentication.getName(), chatId))
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "User isn't member of the chat");
return messageRepository.findByChat_Id(chatId).stream().map(messageMapper::makeDto).toList(); return messageRepository.findByChat_Id(chatId).stream().map(messageMapper::makeDto).toList();
} }
@Override @Override
public MessageTo addToChat(Authentication authentication, Long chatId, MessageCreateTo messageCreateTo) { public MessageTo addToChat(Authentication authentication, Long chatId, MessageCreateTo messageCreateTo) {
if (!chatService.isMemberOf(authentication.getName(), chatId))
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "User isn't member of the chat");
Message message = new Message().setChat(entityManager.getReference(Chat.class, chatId)) Message message = new Message().setChat(entityManager.getReference(Chat.class, chatId))
.setAuthor(entityManager.getReference(Person.class, authentication.getName())) .setAuthor(entityManager.getReference(Person.class, authentication.getName()))
.setContents(messageCreateTo.contents()); .setContents(messageCreateTo.contents());
@@ -56,30 +46,28 @@ public class MessageServiceImpl implements MessageService {
} }
@Override @Override
public MessageTo update(Authentication authentication, Long id, MessageCreateTo msg) { public MessageTo update(Long id, MessageCreateTo msg) {
var message = messageRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); var message = messageRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
if (!Objects.equals(message.getAuthor().getUuid(), authentication.getName()))
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
message.setContents(msg.contents()); message.setContents(msg.contents());
messageRepository.save(message); messageRepository.save(message);
return messageMapper.makeDto(message); return messageMapper.makeDto(message);
} }
@Override @Override
public void delete(Authentication authentication, Long id) { public void delete(Long id) {
var read = messageRepository.findById(id); var read = messageRepository.findById(id);
if (read.isEmpty()) return; if (read.isEmpty()) return;
if (!Objects.equals(read.get().getAuthor().getId(), authentication.getName())) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
messageRepository.delete(read.get()); messageRepository.delete(read.get());
} }
@Override @Override
public Collection<MessageTo> readAll(Authentication authentication) { public Collection<MessageTo> readAll() {
if (!authentication.getAuthorities().contains(new SimpleGrantedAuthority(UserRoles.ROLE_ADMIN.name())))
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
return StreamSupport.stream(messageRepository.findAll().spliterator(), false).map(messageMapper::makeDto).toList(); return StreamSupport.stream(messageRepository.findAll().spliterator(), false).map(messageMapper::makeDto).toList();
} }
@Override
public boolean isAuthorOf(String userUuid, Long messageId) {
var msg = messageRepository.findById(messageId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found"));
return Objects.equals(msg.getAuthor().getUuid(), userUuid);
}
} }

View File

@@ -3,6 +3,7 @@ package com.usatiuk.tjv.y.server.service;
import com.usatiuk.tjv.y.server.dto.PersonCreateTo; import com.usatiuk.tjv.y.server.dto.PersonCreateTo;
import com.usatiuk.tjv.y.server.dto.PersonTo; import com.usatiuk.tjv.y.server.dto.PersonTo;
import com.usatiuk.tjv.y.server.entity.Person; import com.usatiuk.tjv.y.server.entity.Person;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import java.util.Collection; import java.util.Collection;
@@ -20,7 +21,8 @@ public interface PersonService {
PersonTo update(Authentication authentication, PersonCreateTo person); PersonTo update(Authentication authentication, PersonCreateTo person);
void deleteSelf(Authentication authentication); void deleteSelf(Authentication authentication);
void deleteByUuid(Authentication authentication, String uuid); @PreAuthorize("hasRole('ROLE_ADMIN') or authentication.principal.username == #uuid")
void deleteByUuid(String uuid);
Collection<PersonTo> readAll(); Collection<PersonTo> readAll();
@@ -31,7 +33,9 @@ public interface PersonService {
void removeFollower(Authentication authentication, String followee); void removeFollower(Authentication authentication, String followee);
Collection<PersonTo> getAdmins(); Collection<PersonTo> getAdmins();
void addAdmin(Authentication caller, String uuid); @PreAuthorize("hasRole('ROLE_ADMIN') ")
void removeAdmin(Authentication caller, String uuid); void addAdmin(String uuid);
@PreAuthorize("hasRole('ROLE_ADMIN') ")
void removeAdmin(String uuid);
} }

View File

@@ -6,19 +6,17 @@ import com.usatiuk.tjv.y.server.dto.converters.PersonMapper;
import com.usatiuk.tjv.y.server.entity.Chat; import com.usatiuk.tjv.y.server.entity.Chat;
import com.usatiuk.tjv.y.server.entity.Person; import com.usatiuk.tjv.y.server.entity.Person;
import com.usatiuk.tjv.y.server.repository.PersonRepository; import com.usatiuk.tjv.y.server.repository.PersonRepository;
import com.usatiuk.tjv.y.server.security.UserRoles;
import com.usatiuk.tjv.y.server.service.exceptions.UserAlreadyExistsException; import com.usatiuk.tjv.y.server.service.exceptions.UserAlreadyExistsException;
import com.usatiuk.tjv.y.server.service.exceptions.UserNotFoundException; import com.usatiuk.tjv.y.server.service.exceptions.UserNotFoundException;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import java.util.Collection; import java.util.Collection;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
@@ -89,32 +87,25 @@ public class PersonServiceImpl implements PersonService {
return personMapper.makeDto(found); return personMapper.makeDto(found);
} }
private void deleteByUuid(String uuid) { @Transactional
public void deleteByUuid(String uuid) {
var person = personRepository.findById(uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); var person = personRepository.findById(uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
for (Chat c : person.getChats()) { for (Chat c : person.getChats()) {
c.getMembers().remove(person); c.getMembers().remove(person);
} }
for (Person p : person.getFollowers()) { for (Person p : person.getFollowers()) {
p.getFollowing().remove(person); p.getFollowing().remove(person);
personRepository.save(p);
} }
personRepository.delete(person); personRepository.delete(person);
} }
@Override @Override
@Transactional
public void deleteSelf(Authentication authentication) { public void deleteSelf(Authentication authentication) {
deleteByUuid(authentication.getName()); deleteByUuid(authentication.getName());
} }
@Override
public void deleteByUuid(Authentication authentication, String uuid) {
if ((!Objects.equals(authentication.getName(), uuid)) &&
!authentication.getAuthorities().contains(new SimpleGrantedAuthority(UserRoles.ROLE_ADMIN.name())))
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
deleteByUuid(uuid);
}
@Override @Override
public Collection<PersonTo> readAll() { public Collection<PersonTo> readAll() {
@@ -148,24 +139,15 @@ public class PersonServiceImpl implements PersonService {
} }
@Override @Override
public void addAdmin(Authentication caller, String uuid) { public void addAdmin(String uuid) {
if (!caller.getAuthorities().contains(new SimpleGrantedAuthority(UserRoles.ROLE_ADMIN.name())))
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
var person = personRepository.findById(uuid).orElseThrow(UserNotFoundException::new); var person = personRepository.findById(uuid).orElseThrow(UserNotFoundException::new);
person.setAdmin(true); person.setAdmin(true);
personRepository.save(person); personRepository.save(person);
} }
@Override @Override
public void removeAdmin(Authentication caller, String uuid) { public void removeAdmin(String uuid) {
if (!caller.getAuthorities().contains(new SimpleGrantedAuthority(UserRoles.ROLE_ADMIN.name())))
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
var person = personRepository.findById(uuid).orElseThrow(UserNotFoundException::new); var person = personRepository.findById(uuid).orElseThrow(UserNotFoundException::new);
// TODO
if (personRepository.findByAdminIsTrue().size() == 1) return;
person.setAdmin(false); person.setAdmin(false);
personRepository.save(person); personRepository.save(person);
} }

View File

@@ -2,14 +2,18 @@ package com.usatiuk.tjv.y.server.service;
import com.usatiuk.tjv.y.server.dto.PostCreateTo; import com.usatiuk.tjv.y.server.dto.PostCreateTo;
import com.usatiuk.tjv.y.server.dto.PostTo; import com.usatiuk.tjv.y.server.dto.PostTo;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import java.util.Collection; import java.util.Collection;
public interface PostService { public interface PostService {
PostTo createPost(Authentication authentication, PostCreateTo postCreateTo); PostTo createPost(Authentication authentication, PostCreateTo postCreateTo);
PostTo updatePost(Authentication authentication, Long id, PostCreateTo postCreateTo); @PreAuthorize("@postService.isAuthorOf(authentication.principal.username, #id)")
void deletePost(Authentication authentication, Long id); PostTo updatePost(Long id, PostCreateTo postCreateTo);
@PreAuthorize("@postService.isAuthorOf(authentication.principal.username, #id)")
void deletePost(Long id);
PostTo readById(Long id); PostTo readById(Long id);
@@ -18,5 +22,8 @@ public interface PostService {
Collection<PostTo> readByPersonFollowees(Authentication authentication); Collection<PostTo> readByPersonFollowees(Authentication authentication);
Collection<PostTo> readAll(Authentication authentication); @Secured({"ROLE_ADMIN"})
Collection<PostTo> readAll();
boolean isAuthorOf(String userUuid, Long postId);
} }

View File

@@ -6,11 +6,9 @@ import com.usatiuk.tjv.y.server.dto.converters.PostMapper;
import com.usatiuk.tjv.y.server.entity.Person; import com.usatiuk.tjv.y.server.entity.Person;
import com.usatiuk.tjv.y.server.entity.Post; import com.usatiuk.tjv.y.server.entity.Post;
import com.usatiuk.tjv.y.server.repository.PostRepository; import com.usatiuk.tjv.y.server.repository.PostRepository;
import com.usatiuk.tjv.y.server.security.UserRoles;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
@@ -18,7 +16,7 @@ import java.util.Collection;
import java.util.Objects; import java.util.Objects;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
@Service @Service("postService")
public class PostServiceImpl implements PostService { public class PostServiceImpl implements PostService {
private final PostRepository postRepository; private final PostRepository postRepository;
private final PostMapper postMapper; private final PostMapper postMapper;
@@ -60,30 +58,28 @@ public class PostServiceImpl implements PostService {
} }
@Override @Override
public PostTo updatePost(Authentication authentication, Long id, PostCreateTo postCreateTo) { public PostTo updatePost(Long id, PostCreateTo postCreateTo) {
var post = postRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); var post = postRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
if (!Objects.equals(post.getAuthor().getUuid(), authentication.getName()))
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
post.setText(postCreateTo.text()); post.setText(postCreateTo.text());
postRepository.save(post); postRepository.save(post);
return postMapper.makeDto(post); return postMapper.makeDto(post);
} }
@Override @Override
public void deletePost(Authentication authentication, Long id) { public void deletePost(Long id) {
var read = postRepository.findById(id); var read = postRepository.findById(id);
if (read.isEmpty()) return; if (read.isEmpty()) return;
if (!Objects.equals(read.get().getAuthor().getId(), authentication.getName())) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
postRepository.delete(read.get()); postRepository.delete(read.get());
} }
@Override @Override
public Collection<PostTo> readAll(Authentication authentication) { public Collection<PostTo> readAll() {
if (!authentication.getAuthorities().contains(new SimpleGrantedAuthority(UserRoles.ROLE_ADMIN.name())))
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
return StreamSupport.stream(postRepository.findAll().spliterator(), false).map(postMapper::makeDto).toList(); return StreamSupport.stream(postRepository.findAll().spliterator(), false).map(postMapper::makeDto).toList();
} }
@Override
public boolean isAuthorOf(String userUuid, Long postId) {
var p = postRepository.findById(postId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found"));
return Objects.equals(p.getAuthor().getUuid(), userUuid);
}
} }

View File

@@ -82,7 +82,7 @@ public abstract class DemoDataDbTest {
new Person() new Person()
.setUsername("person1") .setUsername("person1")
.setFullName("Person 1") .setFullName("Person 1")
.setPassword(passwordEncoder.encode(person1Password))); .setPassword(passwordEncoder.encode(person1Password)).setAdmin(true));
person1Auth = new TokenResponseTo(jwtTokenService.generateToken(person1.getUuid())); person1Auth = new TokenResponseTo(jwtTokenService.generateToken(person1.getUuid()));
person2 = personRepository.save( person2 = personRepository.save(
new Person() new Person()

View File

@@ -0,0 +1,47 @@
package com.usatiuk.tjv.y.server.controller;
import com.usatiuk.tjv.y.server.dto.ErrorTo;
import com.usatiuk.tjv.y.server.dto.MessageTo;
import com.usatiuk.tjv.y.server.repository.MessageRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
public class MessageControllerTest extends DemoDataDbTest {
@Autowired
MessageRepository messageRepository;
@Test
void shouldGetMessagesIfAdmin() {
var response = restTemplate.exchange(addr + "/message", HttpMethod.GET,
new HttpEntity<>(createAuthHeaders(person1Auth)),
MessageTo[].class);
Assertions.assertNotNull(response);
Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
var toResponse = response.getBody();
Assertions.assertNotNull(toResponse);
Assertions.assertEquals(toResponse.length, messageRepository.findAll().spliterator().estimateSize());
}
@Test
void shouldNotGetMessagesIfNotAdmin() {
var response = restTemplate.exchange(addr + "/message", HttpMethod.GET,
new HttpEntity<>(createAuthHeaders(person2Auth)),
ErrorTo.class);
Assertions.assertNotNull(response);
Assertions.assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode());
var toResponse = response.getBody();
Assertions.assertNotNull(toResponse);
Assertions.assertEquals(toResponse.code(), HttpStatus.FORBIDDEN.value());
}
}

View File

@@ -68,6 +68,17 @@ public class PersonControllerTest extends DemoDataDbTest {
Assertions.assertEquals(personToResponse.fullName(), person1.getFullName()); Assertions.assertEquals(personToResponse.fullName(), person1.getFullName());
} }
@Test
void shouldDeleteSelf() {
var response = restTemplate.exchange(addr + "/person/self",
HttpMethod.DELETE, new HttpEntity<>(createAuthHeaders(person2Auth)), Object.class);
Assertions.assertNotNull(response);
Assertions.assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
Assertions.assertFalse(personRepository.existsById(person2.getUuid()));
}
@Test @Test
void shouldGetFollowers() { void shouldGetFollowers() {
var response = restTemplate.exchange(addr + "/person/followers", var response = restTemplate.exchange(addr + "/person/followers",