I'm designing a db model where I want to have:
User(id, role_id) (UserDetails)
Permission(role_id, permission) (GrantedAuthority)
Role(id, description)
I'm using Hibernate and Spring security. I want every role to have list of permissions (GrantedAuthorities) using role_id rather than specific user having that. I'm kinda lost designing that. This is what I've came up with so far:
public class User implements UserDetails, Serializable {
#Id
#GeneratedValue
#Column(name = "id")
private int id;
#Column(name = "role_id", insertable = true, updatable = false)
private int roleId;
#OneToMany(fetch = FetchType.EAGER, mappedBy = "user", cascade = CascadeType.ALL)
private List<Permission> permissions;
}
public class Permission implements GrantedAuthority, Serializable {
#Id
#GeneratedValue
#Column(name = "id")
private int id;
#Column(name = "role_id", insertable = false, updatable = false)
private int roleId;
#Column(name = "permission")
private String permission;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "role_id", nullable = false, insertable = false, updatable = false)
private User user;
}
I've omitted the not so important code. The Role class isn't important for Spring security.
I know I'm making a huge mistake somewhere but I just can't seem to figure out how to fix it. The issue is that it joins those two objects using user.id instead of user.roleId. I've tried different annotations but I just can't hook it up correctly.
So the raw issue probably is that I'm trying to join 2 objects using a property from one and PK from another which might be a mistake.
Edit: I've also tried to specify referencedColumnName on Permission class but it didn't work either. User can't log in.
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "role_id", nullable = false, insertable = false, updatable = false, referencedColumnName = "role_id")
private User user;
Why would you want to have a design that can backfire you in the future? You really have to think ahead and apply good design practices on your project. This topic pops up here almost every day.
In Spring context, a role actually is an authority. I have no idea why this is made so complex by design by them. You either can have a 1) very simple approach where you assign a role which in fact is authority to an user or 2) more complex solution which includes user, role and permission. The idea would be to assign permissions to roles and assign roles to user. In this solution role entity only serves a purpose of grouping the granted permissions together as one bundle, but through your authentication manager you assign permissions through roles.
Note: I am using common base #MappedSuperclass for my entities.
First, have an User entity:
#Entity
#Table(name = "user_t")
public class User extends BaseEntity {
#Column(name = "username", nullable = false, unique = true)
private String userName;
#Column(name = "password", nullable = false)
private String password;
#ManyToMany
#JoinTable(name = "user_role", joinColumns = #JoinColumn(name = "user_id"), inverseJoinColumns = #JoinColumn(name = "role_id"))
private Set<Role> role = new HashSet<Role>();
// builder/getters/setters
}
Role entity
#Entity(name = "role_t")
#Column(name = "role_name", nullable = false)
private String roleName;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "role_permission", joinColumns = #JoinColumn(name = "role_id"), inverseJoinColumns = #JoinColumn(name = "permission_id"))
private Set<Permission> permissions;
// getters/setters
}
Permission entity
#Entity(name = "permission_t")
public class Permission extends BaseEntity implements GrantedAuthority {
#Column (name = "permission_name", nullable = false)
private String permissionName;
public String getPermissionName() {
return permissionName;
}
public void setPermissionName(String permissionName) {
this.permissionName = permissionName;
}
#Override
public String getAuthority() {
return permissionName;
}
#Override
public int hashCode() {
return permissionName.hashCode();
}
#Override
public boolean equals(Object obj) {
if(obj == null) return false;
if(!(obj instanceof Permission)) return false;
return ((Permission) obj).getAuthority().equals(permissionName);
}
Now in your AuthenticationManager or whatever you decide to use, you loop throug the roles and assign the permissions that are assigned to the roles to the user, if that makes sense.
CustomAuthenticationProvider
public class AppAuthProvider implements AuthenticationProvider {
private static final String PERMISSION_PREFIX = "ROLE_PERMISSION_";
// get the logging user info
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Collection<GrantedAuthority> permissions = new HashSet<GrantedAuthority>();
for (Role role : user.getRole()) {
for (Permission perm : role.getPermissions()) {
GrantedAuthority permission = new SimpleGrantedAuthority(PERMISSION_PREFIX + perm.getPermissionName());
permissions.add(permission);
}
}
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(user, null, permissions); // user object you get from service/repository
return authToken;
}
}
I think you have to re-consider your schema, see the image below. work from here. You need a many-many mapping table for mapping permissions to role.
public class User implements UserDetails, Serializable {
#Id
#GeneratedValue
#Column(name = "id")
private int id;
#JoinColumn(name = "role_id", mappedBy= 'user", insertable = true, updatable = false)
private Role role;
}
public class Permission implements Serializable {
#Id
#GeneratedValue
#Column(name = "id")
private int id;
#Column(name = "permission")
private String permission;
#ManyToMany(fetch=FetchType.LAZY,cascade=CascadeType.ALL)
#JoinTable(name = "role_permissions", catalog = "schema", joinColumns = {
#JoinColumn(name = "id", nullable = false, updatable = false) }, inversJoinColumns =
{ #JoinColumn(name = "id", nullable = false, updatable = false)}
private Set<Role> roles;
}
public class Role implements Serializable {
#Id
#GeneratedValue
#Column(name = "id")
private int id;
#Column(name = "permission")
private String permission;
#ManyToMany(fetch=FetchType.LAZY, mappedBy = "permission")
public Set<Permissions> permisions;
}
Update: see the new schema diagram.
Related
Im learning, and so far i created many to many bidirectional database - user can create many groups and group can have many users - and i cannot find a way for my GroupsController Post mapping to work, from my understanding, it requires to get firstly Users id, in order to set the right relationship in Join table for Group, because the relationship should be set only when user create/join group, not when user create sign up procedure. Postman throws 500 and intelliJ:
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.NullPointerException: Cannot invoke "java.lang.Long.longValue()" because the return value of "com.ilze.highlight.entity.Groups.getId()" is null] with root cause
java.lang.NullPointerException: Cannot invoke "java.lang.Long.longValue()" because the return value of "com.ilze.highlight.entity.Groups.getId()" is null
I use lombok - #Data, #Getter, therefore getId() should be available for use from Group class. My GroupsController with POST mapping when user decides to create a new group:
#RestController
#RequestMapping("api/groups") // pre-path
public class GroupsController{
#Autowired
private GroupsService groupsService;
#Autowired
private UserService userService;
#Autowired
private final GroupsRepository groupsRepository;
#Autowired
private UserRepository userRepository;
public GroupsController(GroupsRepository groupsRepository) {
this.groupsRepository = groupsRepository;
}
#GetMapping("/all-groups")
public List<Groups> getGroups(){
return (List<Groups>) groupsRepository.findAll();
}
#PostMapping("/user/{usersId}/create-group")
public ResponseEntity<Groups> createGroup(#PathVariable(value = "usersId") Long usersId, #RequestBody Groups groupRequest){
Groups group = userRepository.findById(usersId).map(users -> {
long groupsId = groupRequest.getId();
// add and create new group
users.addGroup(groupRequest);
return groupsRepository.save(groupRequest);
}).orElseThrow(() -> new ResourceNotFoundException("Not found user with id = " + usersId));
return new ResponseEntity<>(group, HttpStatus.CREATED);
}
}
Group database class:
#Data
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Getter
#Table(name = "group_collection")
public class Groups {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name ="group_name", unique = true, nullable = false, length = 20)
private String groupName;
#Column(name = "size", nullable = false)
private int size;
#Column(name = "strict", nullable = false)
private boolean strict;
#Column(name = "open", nullable = false)
private boolean open;
#Column(name ="description", length = 300)
private String description;
#Column(name = "create_time", nullable = false)
private LocalDateTime createTime;
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE,
CascadeType.DETACH,
CascadeType.REFRESH
},
mappedBy = "groups")
#JsonIgnore
private Set<User> users = new HashSet<>();
public Set<User> getUsers() {
return users;
}
public void setUsers(Set<User> users) {
this.users = users;
}
}
And Users class for database:
#Data
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "username", unique = true, nullable = false, length = 100)
private String username;
#Column(name = "password", nullable = false)
private String password;
#Column(name = "email", nullable = false)
private String email;
#Column(name = "create_time", nullable = false)
private LocalDateTime createTime;
#Enumerated(EnumType.STRING)
#Column(name = "role", nullable = false)
private Role role;
#Transient
private String accessToken;
#Transient
private String refreshToken;
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE,
CascadeType.DETACH,
CascadeType.REFRESH
})
#JoinTable(name = "groups_x_user",
joinColumns = { #JoinColumn(name = "users_id") },
inverseJoinColumns = {#JoinColumn(name = "groups_id")})
private Set<Groups> groups = new HashSet<>();
public void addGroup(Groups group) {
this.groups.add(group);
group.getUsers().add(this);
}
public void removeGroup(long id){
Groups group = this.groups.stream().filter(g ->
g.getId() == id).findFirst().orElse(null);
if(group != null){
this.groups.remove(group);
group.getUsers().remove(this);
}
}
For reference my GroupsService implementation:
#Service
public class GroupsServiceImpl implements GroupsService{
private final GroupsRepository groupsRepository;
public GroupsServiceImpl(GroupsRepository groupsRepository) {
this.groupsRepository = groupsRepository;
}
#Override
public Groups saveGroup(Groups group) {
group.setCreateTime(LocalDateTime.now());
return groupsRepository.save(group);
}
#Override
public Optional<Groups> findByGroupName(String groupName) {
return groupsRepository.findByGroupName(groupName);
}
}
You need to persist the object from request. And since you have Many-2-Many relation, you can insert related object from both sides. In your case: just add existing user to the newly created group
The method will look something like that:
#PostMapping("/user/{usersId}/groups")
public ResponseEntity<Groups> createGroup(#PathVariable(value = "usersId") Long usersId, #RequestBody Groups groupRequest) {
Groups createdGroup = userRepository.findById(usersId)
.map(user -> {
groupRequest.setId(null); // ID for new entry will be generated by entity framework, prevent override from outside
groupRequest.getUsers().add(user); // add relation
return groupsRepository.save(groupRequest);
}).orElseThrow(() -> new ResourceNotFoundException("Not found user with id = " + usersId));
return new ResponseEntity<>(createdGroup, HttpStatus.CREATED);
}
i have Three entities User, Institution and Role.
1)one to many between user and institution
2)and many to many between User and Role
-------user-------
#Entity
#Table(name = "user")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="user_Id")
private int userId;
#Column(name="name")
private String name;
#Column(name="lastname")
private String lastname;
#Column(name="email")
private String email;
#Column(name="password")
private String password;
#Column(name="isActive")
private boolean isActive;
#Column(name="lastActive")
private String lastActive;
#Column(name="createdDate")
private String createdDate;
#Column(name="isBlocked")
private boolean isBlocked;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "institution_id", nullable = false)
#JsonIgnoreProperties(value = {"user"})
private Institution institution;
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "user_has_role",
joinColumns = {
#JoinColumn(name = "user_id", referencedColumnName = "user_id",
nullable = false, updatable = true)},
inverseJoinColumns = {
#JoinColumn(name = "role_id", referencedColumnName = "role_id",
nullable = false, updatable = true)})
#JsonIgnoreProperties(value = {"users"})
private Set<Role> roles = new HashSet<>();
}
-------institution-------
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#ToString
#Entity
#Table(name = "institution")
public class Institution {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="institution_Id")
private int institutionId;
#Column(name="name")
private String name;
#Column(name="type")
private String type;
#Column(name="location")
private String location;
#OneToMany(mappedBy = "institution", fetch = FetchType.LAZY)
#JsonIgnoreProperties(value = {"institution" , "user"})
private Set<User> user;
}
-------role-------
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#ToString
#Entity
#Table(name = "role")
public class Role {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="role_Id")
private int roleId;
#Column(name="name")
private String name;
#Column(name="description")
private String description;
#ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
#JsonIgnoreProperties(value = {"roles"})
private Set<User> users = new HashSet<>();
}
Those are my 3 entities and tables in MySql
i have 7 roles
• Super-User
• Bank-Admin
• Bank-Support
• Bank-Service
• Merchant-Admin
• Merchant-Support
• Merchant-service
The super-User can create a user of any role
#PostMapping("/addUser")
public String addUser(#RequestBody User user) {
String rawpassword = user.getPassword();
String encodedpasswrod = passwordencoder.encode(rawpassword);
user.setPassword(encodedpasswrod);
userrepository.save(user);
return "user saved with name: " + user.getName();
}
this api works and i can set the role to anything in my api json body
But want that if the User is Bank-Admin he can only create Bank-Support and Bank-Service
im trying to create a new API which can only create a user with those 2 specific roles.
and then restrict the bank admin to access the other API that can create users of any kind.
is there any other way to do it and if no how can i do that...
You have to implement your custom implementation of User Entitlement.
Like according to login person, you will get that login person role, and according to your criteria just put validation like check that entity he is trying to add is he eligible to create it.
Map<String, List<String>> roleUserAccessMap = new HashMap<>();
roleUserAccessMap.put("Bank-Admin", Arrays.asList("Bank-Support", "Bank-Service"));
Just check like below
String loginPersonRole="Bank-Admin"; //This value should get from logged-in person context
if(roleUserAccessMap.containsKey(loginPersonRole) && roleUserAccessMap.get(loginPersonRole).contains(newuserrole) ){
//proceed ahead with Add api
}else{
System.out.println("You do not have enough privileage to create Use");
}
This will help you.
I am getting a JSON which is deserialised into a POJO and I am trying to map a POJO to below entity class using modelmapper and trying to save these entities into the database.All the fields are getting mapped and saved as expected except the foreign key value which is coming as null. Also, the userid is auto generated.Can you please help me how can I save the user id in vehicle table using modelmapper??
My User Entity class
#Entity
#Table(name = "usr")
class User{
#Id
#Column(name = "user_id", updatable = false, nullable = false)
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long userId;
private String username;
#OneToMany(mappedBy = "user",cascade = CascadeType.ALL)
private List<Vehicle> vehicles= new ArrayList<>();
//getters and setters
}
My Vehicles Entity Class:
#Entity
#Table(name = "vehicle")
class Vehicle{
#Id
#Column(name = "vehicle_id", updatable = false, nullable = false)
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long vehicleId;
private String vehicleName;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_fk" , nullable = false)
private User user;
//getters and setters
}
My ModelMapping class:
#Service
public class UserService {
#Autowired
private UserDetailsService userDetailsService;
public void saveUserDetails(UserDetails userDetails
User targetuser{
UserDetails sourcePojo= userDetails
User targetEntity= targetuser
ModelMapper modelMapper = new ModelMapper();
modelMapper.map(sourcePojo, targetEntity);
userDetailsService.save(targetEntity);
}
}
You Can try with making annotation more specific this :
#OneToMany(mappedBy = "user",
targetEntity = Vehicle.class,
cascade = CascadeType.ALL)
private List<Vehicle> vehicles= new ArrayList<>();
And
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_fk" ,
referencedColumnName = "user_id",
nullable = false)
private User user;
And I think you have to remove annotation for auto generation of userid
As per my understanding issue is due to Auto generation of userid is done while saving an entry to DB. But when we mapped userid to vehicle it is null as it is not generated.
I have a spring service class where I'm loading a JPA object (target) via CRUD. This target class has a one-to-may mapping that is set to lazy loading.
I would like to query this object inside a spring service method that is annotated with #Transactional and avoid that the childs are being loaded.
When I execute the following code all child data is loaded and laziness is ignored.
#Override
#Transactional
public boolean changeState(boolean enabled, final EventType eventType, final String deviceSerialNumber) {
final UniqueUser user = userService.getUser();
final Target target = targetRepository.findEventsByUserIdAndTenantIdAndTargetDeviceId(user.getUserId(), user.getTenantId(), deviceSerialNumber);
//here everything gets loaded
if (target == null) {
return false;
}
final List<EventHistory> events = target.getEvents().stream()
.filter(event -> event.getEventType() == eventType)
.filter(event -> event.isActive() != enabled)
.collect(Collectors.toList());
events.forEach(event -> event.setActive(enabled));
if (events.isEmpty()) {
return false;
}
return true;
}
Mappings:
#ToString
#Entity
#Table(name = "target")
public class Target {
#Id
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "org.hibernate.id.UUIDGenerator")
#Column(name = "id", unique = true)
private UUID id;
#Column(name = "user_id")
private String userId;
#Column(name = "tenant_id")
private String tenantId;
#Column(name = "target_device_id")
private String targetDeviceId;
#Column(name = "target_type")
private TargetType targetType;
#OneToMany(mappedBy = "target", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#JsonManagedReference
private List<EventHistory> events = new ArrayList<>();
public void addEvents(EventHistory event) {
events.add(event);
event.setTarget(this);
}
}
#Entity
#Data
#Table(name = "event_history")
public class EventHistory {
#Id
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "org.hibernate.id.UUIDGenerator")
#Column(name = "id", unique = true)
private UUID id;
#Column(name = "active")
private boolean active;
#OneToMany(mappedBy = "event", cascade = CascadeType.ALL, orphanRemoval = true)
#JsonManagedReference
private List<EventTimestamp> timestamps = new ArrayList<>();
#ManyToOne(optional = false, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "target_id", nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
#JsonBackReference
private Target target;
public void addTimestamps(EventTimestamp eventTimestamp) {
timestamps.add(eventTimestamp);
eventTimestamp.setEvent(this);
}
}
#Entity
#Data
#Table(name = "event_timestamp")
public class EventTimestamp {
#Id
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "org.hibernate.id.UUIDGenerator")
#Column(name = "id", unique = true)
private UUID id;
#Column(name = "due_timestamp")
private Timestamp dueTimestamp;
#Column(name = "period")
private String period;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "event_id", nullable = false)
#JsonBackReference
private EventHistory event;
So my question is how to keep lazy loading inside transaction annotated functions?
My first assumption that the root cause had been the wrongly implemented repository function was wrong. The real issue was the #ToString annotation. This added the one-to-many event collection to toString(). While being inside the transaction and accessing the object, toString got invoked and the the collection was loaded.
Solution was to exclude the collection from toString via.
#OneToMany(mappedBy = "target", cascade = CascadeType.ALL, orphanRemoval = true)
#ToString.Exclude
private List<EventHistory> events;
I figured it out. The problem was ins the Repository code. The findBy method is expecting a List instead of a single object.
My original repository looked like this
#Repository
public interface TargetRepository extends CrudRepository<Target, UUID> {
Target findEventsByUserIdAndTenantIdAndTargetDeviceId(String userId, String tenantId, String targetId);
}
Changing it to the below version fixed it.
#Repository
public interface TargetRepository extends CrudRepository<Target, UUID> {
List<Target> findEventsByUserIdAndTenantIdAndTargetDeviceId(String userId, String tenantId, String targetId);
}
I need to add Spring Security into my project. What is the right way to do it? I have to entities User and UserRole and DAO and Services for them. I use EntityManager to access data. I read, that I just need to write implementation for UserDetails, but I don't know how to do it correctly. Here my code:
User.java
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonProperty
private Integer id;
#Column(name = "username", length = 20, nullable = false)
#JsonProperty
private String username;
#Column(name = "password", nullable = false)
#JsonProperty
private String password;
#Column(name = "enabled", nullable = false)
#JsonProperty
private boolean enabled;
#Column(name = "email", nullable = false)
#JsonProperty
private String email;
#OneToMany(mappedBy = "user", cascade = {CascadeType.ALL}, fetch = FetchType.EAGER)
private Set<UserRole> userRoles;
//getters and setters
UserRole.java
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonProperty
private Integer id;
#ManyToOne(optional = false)
#JoinColumn(name = "user_ID", referencedColumnName = "id", foreignKey = #ForeignKey(ConstraintMode.NO_CONSTRAINT))
private User user;
#Column(name="role")
#JsonProperty
private String role;
//getters and setters
What should I do?
I had wrote a blog post about exactly what you are looking for. see this post and I am pretty sure it will answer your question:
https://giannisapi.wordpress.com/2011/09/21/spring-3-spring-security-implementing-custom-userdetails-with-hibernate/
In the Service layer of UserDetails below, pay attention that it implements UserDetailsService from org.springframework.security.core.userdetails.UserDetailsService.
and also :
he loadUserByUsername methods return the result of the assembler.buildUserFromUserEntity . Simply put, what this method of the assembler does is to to construct a org.springframework.security.core.userdetails.User object from the given UserEntity DTO. The code of the Assembler class is given below: