Let's say I have 2 have to entities:
#Entity
public class Post {
#NotEmpty
private String title;
#NotEmpty
#Lob
private String html;
#NotEmpty
#Lob
private String text;
#ManyToOne
private Topic topic;
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(name = "content_media", joinColumns = {#JoinColumn(name = "content_id")}, inverseJoinColumns = {#JoinColumn(name = "media_id")})
private Set<Media> medias = new HashSet<>();
#CreatedBy
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn
private User createdBy;
#LastModifiedBy
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn
private User lastModifiedBy;
...
}
#Entity
public class Media {
#NotEmpty
private String localPath;
#NotEmpty
private String fileName;
private long fileLength;
private String fileType;
private int focusPointX;
private int focusPointY;
...
}
And I'm exposing them using:
#RepositoryRestController
public interface MediaRepository extends JpaRepository<Media, Long> {
}
#RepositoryRestController
public interface PostRepository extends JpaRepository<Post, Long> {
}
I want these controllers to be secure. Let me explain myself.
If logged in user does not have ROLE_ADMIN, Medias should only be
accessable through posts and /medias/ should return 403 or 404
Only users that have ROLE_USER should be able to create to posts
Only the user that have created the post or the ones that have the ROLE_ADMIN should be able to update post.
Only the users that have ROLE_ADMIN should be able to delete posts
Is there a way to do these using RepositoryRestController and Spring Security or RepositoryRestController is for public resources only and I should write service layer myself using RestController?
Yes you can directly use Spring Security with Spring Data REST. You need to define the security of your routes using Spring Security Configuration as shown below:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests().
antMatchers(HttpMethod.POST, "/posts").hasRole("USER").
antMatchers(HttpMethod.DELETE, "/posts/**").hasRole("ADMIN").and().
csrf().disable();
}
}
Repository methods will be secured using Spring Security annotations. e.g.
#RepositoryRestController
public interface PostRepository extends JpaRepository<Post, Long> {
#Override
#PreAuthorize("hasRole('ROLE_ADMIN')")
void delete(Long aLong);
}
Code above is just a pointer. You can customize it as per your needs. Here is the link to Spring Data examples repository.
Update
To handle the update of the post by the user who created or by any user who is in ADMIN_ROLE you need to create a controller class and define a method with to handle the update
#RequestMapping(method={RequestMethod.PUT}, value={"posts/{id}"})
public void updatePost(#PathVariable("id") Long id, HttpServletRequest request)
{
//Fetch the authenticated user name
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername();
}
// Make a database call to verify if the user is owner of the post
Post post = postRepository.getPostByUserName(String username, Long postId);
if (post == null && !request.isUserInRole("ADMIN");) {
//return 403 error code
}
//proceed with the update
}
Related
So I have an audit class that is using #MappedSuperClass, it updates the values createdBy and updatedBy but it does not add a foreign key on the User entity, so there's no database validation
Here's the Audit Class
#Getter
#Setter
#MappedSuperclass
#JsonIgnoreProperties(
value = {"createdBy", "updatedBy"},
allowGetters = true
)
public abstract class UserDateAudit extends DateAudit {
#CreatedBy
#Column(name = "created_by", nullable = false, updatable = false)
public Long createdBy;
#LastModifiedBy
#Column(name = "updated_by", nullable = false)
public Long updatedBy;
}
I searched on how to do this using #Inheritance but did not completely get it.
so How do I achieve the connection between this class and User entity?
Edit 1
Here's the auditing configuration I implement.
#Configuration
#EnableJpaAuditing
public class AuditingConfig {
#Bean
public AuditorAware<Long> auditorProvider() {
return new SpringSecurityAuditAwareImpl();
}
}
class SpringSecurityAuditAwareImpl implements AuditorAware<Long> {
#Override
public Optional<Long> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null ||
!authentication.isAuthenticated() ||
authentication instanceof AnonymousAuthenticationToken) {
return Optional.empty();
}
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
return Optional.ofNullable(userPrincipal.getId());
}
}
Edit 2
to set clear what exactly I mean
so how to use something other than #MappedSuperclass to be able achieve this "I want to be able to map the user reference in all tables that inherit the UserDateAudit so that it is a foreign key for all these table (which would add a validation that the user id actually exist) not just a regular column".
You need to implement the org.springframework.data.domain.AuditorAware interface and add the logic to retrieve currently logged in user in the getCurrentAuditor method.
#Component
#RequiredArgsConstructor
public class AuditorResolver implements AuditorAware<YOUR_TYPE> {
#Override
public Optional<YOUR_TYPE> getCurrentAuditor() {
//code to retrieve the currently logged in user and return the id/user object
}
}
See this for an example: https://github.com/gtiwari333/spring-boot-blog-app/blob/master/src/main/java/gt/app/config/AuditorResolver.java
The documentation also describes it beautifully: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#auditing.basics
I wrote a few request mappings and connected my Spring Boot application to a Postgresql DB using JPA. However, when I try to call the API, I get the following message:
{"timestamp":"2021-01-30T21:58:34.028+00:00","status":404,"error":"Not Found","message":"","path":"/api/v1/sessions"}. I tried printing a message when the API is called and it works so I think it might have something to do with the JPA connection? (I also tested if the DB and credentials are good using SQL Shell, and they are ok)
My model:
#Entity(name = "sessions")
public class Session {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long session_id;
private String session_name;
private String session_description;
private Integer session_length;
#ManyToMany
#JoinTable(
name = "session_speakers",
joinColumns = #JoinColumn(name = "session_id"),
inverseJoinColumns = #JoinColumn(name = "speaker_id"))
private List<Speaker> speakers;
My controller:
#Controller
#RequestMapping("/api/v1/sessions")
public class SessionsController {
#Autowired
private SessionRepository sessionRepository;
#GetMapping
public List<Session> list() {
System.out.println("Get method called");
return sessionRepository.findAll();
}
#GetMapping
#RequestMapping("{id}")
public Session get(#PathVariable Long id) {
return sessionRepository.getOne(id);
}
My repository:
public interface SessionRepository extends JpaRepository<Session, Long> {
}
And lastly, my application properties:
spring.datasource.url=jdbc:postgresql://localhost:5432/databaseName
spring.datasource.username=postgres
spring.datasource.password=mypasswordhere
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.show-sql=true
#GetMapping(value = "/{id}")<---remove Request mapping and also add the slash here
public Session get(#PathVariable Long id) {
return sessionRepository.getOne(id);
}
This question already has answers here:
How to fix Hibernate LazyInitializationException: failed to lazily initialize a collection of roles, could not initialize proxy - no Session
(15 answers)
Closed 2 years ago.
I am trying to fetch user's profile data post login (from login success filter) but I am seeing an exception for Lazy loading the data. Please see the following sample code:
AuthenticationSuccessHandler.java
#Component
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
#Autowired
private UserService userService;
#Autowired
private Gson gson;
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws ServletException, IOException {
User user = (User) authentication.getPrincipal();
UserLoginResponseDto userLoginResponseDto = userService.login(user.getUsername());
response.setStatus(HttpStatus.OK.value());
response.setContentType("application/json; charset=UTF-8");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.getWriter().println(gson.toJson(userLoginResponseDto));
response.getWriter().flush();
clearAuthenticationAttributes(request);
}
}
UserService.java
public class UserService implements UserDetailsService, TokenService {
#Autowired
private UserRepository userRepository;
#Transactional(readOnly = true)
public UserLoginResponseDto login(String email) {
Optional<UserModel> userOptional = userRepository.findByEmailIgnoreCase(email);
UserModel userModel = userOptional.get();
UserLoginResponseDto userLoginResponseDto = userModel.toUserLoginResponseDto();
return userLoginResponseDto;
}
}
UserModel.java
public class UserModel {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(nullable = false, unique = true, updatable = false)
private UUID id;
[A FEW MORE FIELDS]
#Column(length = 256, nullable = false, unique = true, updatable = false)
private String email;
#OneToMany(cascade = { CascadeType.ALL })
private List<RoleModel> roleModels;
public UserLoginResponseDto toUserLoginResponseDto() {
return new UserLoginResponseDto().setId(id).setEmail(email).setRoles(roleModels);
}
}
UserLoginResponseDto.java
public class UserLoginResponseDto {
private UUID id;
private String email;
private List<RoleModel> roles;
}
When an object of type UserLoginResponseDto is serialized in AuthenticationSuccessHandler, I see the following error message -
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: UserModel.roleModels, could not initialize proxy - no Session
QQ - How do I resolve this correctly without employing any of the following techniques?
[ANTIPATTERN] Open-In-View
[ANTIPATTERN] hibernate.enable_lazy_load_no_trans
FetchType.EAGER
Your problem is that you're passing the actual lazy List into setRoles, which doesn't trigger the full load. This indicates (immediately) that while you've separated your top-level database class from your top-level DTO, it's a "shallow" separation, which doesn't fully materialize the values. You haven't shown whether RoleModel is an entity or an embeddable, and that matters.
So the first step is to copy the items into a non-JPA form. If RoleModel embeddable (essentially a POJO), this could be as simple as setRoles(new ArrayList<>(roles)). Otherwise, you need a nested DTO, and at that point might consider something like MapStruct.
In either case, though, you're likely to run into the N+1 problem. You in fact do want an eager fetch in this case, and that's what JPA entity graph is for. You can tell Spring Data to fetch the list eagerly only when you want it, and this is a perfect example of when to do that.
A guy ask me for help him resolve a strange bug in security service of his spring boot app, after hours of trail I manage to fix the bug but I really have no idea what happen. Please look at these classes:
Class User : user infomation in database
#Getter
#Setter
#Entity
#Table(name = "user", uniqueConstraints = {#UniqueConstraint(columnNames = {"user_name"})})
public class User extends DateAudit implements Serializable {
// Id, username, password and constructor... not really important
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "user_roles", joinColumns = #JoinColumn(name = "user_id"), inverseJoinColumns = #JoinColumn(name = "role_id"))
private Set<Role> role = new HashSet<>();
}
Class UserDto : implement Spring's interface for manage user credential
#Getter
#Setter
#AllArgsConstructor
public class UserDto implements org.springframework.security.core.userdetails.UserDetails {
private long id;
private String Username;
#JsonIgnore
private String password;
private Collection<? extends GrantedAuthority> authorities;
public static UserDto create(User user) {
List<GrantedAuthority> authorities = user.getRole().stream()
.map(role -> new SimpleGrantedAuthority(role.getName().name())).collect(Collectors.toList());
return new UserDto(user.getId(), user.getUserName(), user.getPassword(), authorities);
}
// implement interface's methods, only getters, not important
}
Class CustomUserService : Auth service
#Service
public class CustomUserService implements org.springframework.security.core.userdetails.UserDetailsService {
private final UserRepository userRepository;
#Autowired
public CustomUserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
#Override
#Transactional
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
User user = userRepository.findUserByUserName(userName).orElseThrow(() -> {
return null;
});
return UserDto.create(user);
}
}
Full source code can be found here (it is only a simple spring boot app with some classes) : https://github.com/raizo000/admin_project (his repo uses embedded tomcat, I've tried to change it to jetty but it is not the cause)
When I run code the first time with these 3 classes. The line
return UserDto.create(user);
Give me a NoClassDefFoundError :
WARN 7252 --- [qtp223566397-22] org.eclipse.jetty.server.HttpChannel : /login
java.lang.NoClassDefFoundError: com/example/admin/dto/UserDto
at com.example.admin.services.CustomUserService.loadUserByUsername(CustomUserService.java:28) ~[classes/:na]
at com.example.admin.services.CustomUserService$$FastClassBySpringCGLIB$$b9234a59.invoke(<generated>) ~[classes/:na]
....
I have checked the jar file, there is a UserDto.class file in the right directory.
Remove the #Transactional help fix the error but cause another lazily initialize error and I end up change fetch = FetchType.LAZY in User class to fetch = FetchType.EAGER as a quick fix.
Why adding Transactional can cause a NoClassDefFoundError? Do remove Transactional is the right solution or there is a better fix?
When a class is not found, the first thing that you should make is clean and build.
Because, the last build is saving some dependency.
So the #Transactional #interface was not found in some corrupt package.
A rebuild(clean build) solves it.
Given a User entity with the following attributes mapped:
#Entity
#Table(name = "user")
public class User {
//...
#Id
#GeneratedValue
#Column(name = "user_id")
private Long id;
#Column(name = "user_email")
private String email;
#Column(name = "user_password")
private String password;
#Column(name = "user_type")
#Enumerated(EnumType.STRING)
private UserType type;
#Column(name = "user_registered_date")
private Timestamp registeredDate;
#Column(name = "user_dob")
#Temporal(TemporalType.DATE)
private Date dateOfBirth;
//...getters and setters
}
I have created a controller method that returns a user by ID.
#RestController
public class UserController {
//...
#RequestMapping(
value = "/api/users/{id}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<User> getUser(#PathVariable("id") Long id) {
User user = userService.findOne(id);
if (user != null) {
return new ResponseEntity<User>(user, HttpStatus.OK);
}
return new ResponseEntity<User>(HttpStatus.NOT_FOUND);
}
//...
}
A service in my business logic layer.
public class UserServiceBean implements UserService {
//...
public User findOne(Long id) {
User user = userRepository.findOne(id);
return user;
}
//...
}
And a repository in my data layer.
public interface UserRepository extends JpaRepository<User, Long> {
}
This works fine, it returns everything about the user, but I use this in several different parts of my application, and have cases when I only want specific fields of the user.
I am learning spring-boot to create web services, and was wondering: Given the current implementation, is there a way of picking the attributes I want to publish in a web service?
If not, what should I change in my implementation to be able to do this?
Thanks.
Firstly, I agree on using DTOs, but if it just a dummy PoC, you can use #JsonIgnore (jackson annotation) in User attributes to avoid serializing them, for example:
#Entity
#Table(name = "user")
public class User {
//...
#Column(name = "user_password")
#JsonIgnore
private String password;
But you can see there, since you are not using DTOs, you would be mixing JPA and Jackson annotations (awful!)
More info about jackson: https://github.com/FasterXML/jackson-annotations