im trying to secure my endpoints with role based access control. I have implemented the whole structure as well as CustomUserDetailService, however im not sure how should i enforce these rules on enpoints, i was looking for some nice annotation based evaluation like #PreAuthorize(hasRole('role')). My structure looks follwoing:
Permission:
#Entity
public class Permission implements GrantedAuthority {
#Id
private Long id;
#Column(name = "NAME")
private String name;
#ManyToMany(mappedBy = "permissions", fetch = FetchType.LAZY)
private Collection<Role> roles;
#Override
public String getAuthority() {
return name;
}
Role:
#Entity
public class Role implements GrantedAuthority {
#Id #Column(name="ID" )
private Long id;
#Column(name="NAME", nullable=false , unique=false)
private String name;
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(
name = "role_x_permission",
joinColumns = #JoinColumn(
name = "role_id"),
inverseJoinColumns = #JoinColumn(
name = "permission_id"))
private List<Permission> permissions;
#Override
public String getAuthority() {
return name;
}
User:
#Entity(name = "User")
#Table(name = "USERS")
#Data
public class User {
#Id
private Long id;
#Column(name="LOGIN" , nullable=true , unique=false)
private String login;
#Column(name="PASSWORD" , nullable=false , unique=false)
private String password;
#ManyToMany( fetch = FetchType.LAZY)
#JoinTable(name = "USER_ROLES",
joinColumns = #JoinColumn(name = "user_id"),
inverseJoinColumns = #JoinColumn(name = "role_id"))
private Set<Role> roles;
Now i have defined my CustomUserDetailsService:
#Service
public class UserDetailsServiceImpl implements UserDetailsService {
#Autowired
private UserRepository userRepository;
#Override
#Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User applicationUser = userRepository.findByUsername(username);
if (applicationUser.getId() == null) {
throw new UsernameNotFoundException(username);
}
return new org.springframework.security.core.userdetails.User(applicationUser.getLogin(), applicationUser.getPassword(),
getAuthorities(applicationUser.getRoles()));
}
#Transactional
public Collection<? extends GrantedAuthority> getUserAuthorities(String username) {
User user = userRepository.findByUsername(username);
return getAuthorities(user.getRoles());
}
private Collection<? extends GrantedAuthority> getAuthorities(
Collection<Role> roles) {
return getGrantedAuthorities(getPermissions(roles));
}
private List<String> getPermissions(Collection<Role> roles) {
List<String> permissions = new ArrayList<>();
List<Permission> collection = new ArrayList<>();
for (Role role : roles) {
collection.addAll(role.getPermissions());
}
for (Permission item : collection) {
permissions.add(item.getName());
}
return permissions;
}
private List<GrantedAuthority> getGrantedAuthorities(List<String> permissions) {
List<GrantedAuthority> authorities = new ArrayList<>();
for (String permission : permissions) {
authorities.add(new SimpleGrantedAuthority(permission));
}
return authorities;
}
}
Then i'm trying to annotate my endpoint with #PreAuthorize
#PostMapping("/doSomething")
#PreAuthorize("hasRole('doSomething')")
public SomeEntity createComment(#RequestBody SomeEntity something) {
...
}
I have user with role of USER, this role doesn't have permission to doSomething, however it seems like #PreAuthorize("hasRole('doSomething')") is not working. I'm not sure what i have done wrong, coule you please point my mistake?
Also, since im using RBAC this hasRole is very missleading since access is permission based, not role based.
What would be correct way to authorize access to endpoint with RBAC approach?
You should use hasAuthority('doSomething') instead of hasRole('doSomething').
Role is just a permission with a prefix Role_.
So hasRole('XXX') is same as hasAuthority('ROLE_XXX')
Related
I have a Spring Boot project, in which I have problem with JpaRepository<User,Long>.
I defined following interface
public interface UserDAO extends JpaRepository<User, Long> {
User findByEmail(String email);
}
which was implemented in UserServiceImpl
#Override
public User findByEmail(String email) {
return userDAO.findByEmail(email);
}
and I need it in UserDetailsServiceImpl but next line
User user = userDAO.findByEmail(email);
returns null.
My model classes:
User
#Entity
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private long id;
#Column(name = "email")
private String email;
#Column(name = "login")
private String login;
#Column(name = "password")
private String password;
#ManyToMany
#JoinTable(name = "user_roles",
joinColumns = #JoinColumn(name = "user_id"),
inverseJoinColumns = #JoinColumn(name = "role_id"))
private Set<Role> roles;
#Transient
private String confirmPassword;
//getters-setters
}
Role
#Entity
#Table(name = "roles")
public class Role implements GrantedAuthority {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Column(name = "name")
private String name;
#ManyToMany(mappedBy = "roles")
private Set<User> users;
What can I do?
You need to follow JavaBean specification for entity(you need to have default contrucor and getters and setters in User and Role)
Try to use the annotations on the getter, not the fields.
what is the best way which can I add to the following method in a spring boot controller class to allow the user just delete the restaurant he created .
I don't want to add the user id to the path, I want the logged in user to not allowed deleting restaurant which he didn't create.
Note that I extend Auditable to add createdBy to the database mysql
#DeleteMapping("/restaurant/{restaurantId}")
public String deleteRestaurantById(#PathVariable("restaurantId") Long restaurantId) {
if(restaurantService.existsById(id) == false){
logger.info("Error occurred because this restaurant is not found!");
throw new InternalServerErrorException("There is no restaurant with this id");
}
restaurantService.deleteById(id);
return "deleted";
}
Restaurant.java
#Entity
#NoArgsConstructor
#RequiredArgsConstructor
#Getter
#Setter
#ToString
public class Restaurant extends Auditable{
#Id
#GeneratedValue
private long id;
#NonNull
#NotEmpty(message = "The restaurant must have a name")
private String name;
....
}
User.java
#Entity
#RequiredArgsConstructor
#Getter
#Setter
#ToString
#NoArgsConstructor
#PasswordMatch
public class User implements UserDetails {
#Id #GeneratedValue
private Long id;
#NonNull
#Size(min = 8, max = 20)
#Column(nullable = false, unique = true)
private String email;
#NonNull
#Column(length = 100)
private String password;
#Transient
#NotEmpty(message = "Please enter Password Confirmation.")
private String confirmPassword;
#NonNull
#Column(nullable = false)
private boolean enabled;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(
name = "users_roles",
joinColumns = #JoinColumn(name = "user_id",referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "role_id",referencedColumnName = "id")
)
private Set<Role> roles = new HashSet<>();
#NonNull
#NotEmpty(message = "You must enter First Name.")
private String firstName;
#NonNull
#NotEmpty(message = "You must enter Last Name.")
private String lastName;
#Transient
#Setter(AccessLevel.NONE)
private String fullName;
#NonNull
#NotEmpty(message = "Please enter alias.")
#Column(nullable = false, unique = true)
private String alias;
private String activationCode;
public String getFullName() {
return firstName + " " + lastName;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().map(role -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList());
}
public void addRole(Role role) {
roles.add(role);
}
public void addRoles(Set<Role> roles) {
roles.forEach(this::addRole);
}
#Override
public String getUsername() {
return email;
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return enabled;
}
}
You can implement One-to-One relationship between User and Restaurant with join table if User is able to create the only Restaurant.
it is impossible for spring security to magically know what restaurants that are created by a user.
There has to be some form of relationship between the user and the restaurant.
You can either as mentioned before have a relationship between created restaurants and the user that created them.
Or you can have a column named created_by in the restaurant entity and then store the users id in that column. So that when you fetch restaurants you use the id in the Principal to filter on restaurants created by said user.
But expecting spring security to solve this for you is not going to happen.
My program has a user and a role configuration of security, two models that have between them a many-to-many relationship. When I am trying to save a new user and his m-to-m relationship in a separate table named "user_roles", I get the following error on the session.save() function:
org.hibernate.HibernateException: Illegal attempt to associate a collection with two open sessions. Collection : [com.session.library.libraryofbooks.model.Role.employees#1]
Here is my User model code:
#Entity
#Table(name="user")
public class User {
#ManyToMany(cascade = { CascadeType.ALL })
#JoinTable(
name = "user_role",
joinColumns = { #JoinColumn(name = "user_id") },
inverseJoinColumns = { #JoinColumn(name = "role_id") }
)
private Set<Role> roles = new HashSet<>();
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column(name = "username", nullable = false)
private String username;
#Column(name = "password", nullable = false)
private String password;
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
},
mappedBy = "users")
private Set<Book> books = new HashSet<>();
public Set<Book> getBooks() {
return books;
}
public void setBooks(Set<Book> books) {
this.books = books;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
}
Role model:
#Entity
#Table(name="role")
public class Role {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
#ManyToMany(mappedBy = "roles")
private Set<User> employees = new HashSet<>();
}
UserRepository
#Transactional
#Repository
public class UserRepository {
#Autowired
private EntityManagerFactory entityManagerFactory;
public User findByUsername(String username) {
Session session = entityManagerFactory.unwrap(SessionFactory.class).openSession();
Criteria criteria = session.createCriteria(User.class);
User foundUser = (User) criteria.add(Restrictions.eq("username", username)).uniqueResult();
return foundUser;
}
public void saveUser(User user) {
Session session = entityManagerFactory.unwrap(SessionFactory.class).openSession();
session.save(user);
}
}
UserService save user function:
public void saveUser(User user) {
user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
Role userRole = roleRepository.findByName("ROLE_ADMIN");
Set<Role> roleSet = new HashSet<>();
roleSet.add(userRole);
user.setRoles(roleSet);
userRepo.saveUser(user);
}
And the RoleRepository
#Repository("roleRepository")
public class RoleRepository {
#Autowired
private EntityManagerFactory entityManagerFactory;
public Role findByName(String name) {
Session session = entityManagerFactory.unwrap(SessionFactory.class).openSession();
Criteria criteria = session.createCriteria(Role.class);
Role foundRole = (Role) criteria.add(Restrictions.eq("name", name)).uniqueResult();
return foundRole;
}
}
Neither the user datas and the role and the user ids from the user_role table are saving, and I can't figure out what I am doing wrong.
You are using #Transactional but then you use EM to open sessions (and never close it). You should get the current session (SessionFactory.getCurrentSession()).
Anyway I would suggest you to use a pure JPA orm agnostic.
SessionFactory is Hibernate's but you could just use JPA layer and forget about the ORM under the hood.
If you go this way the modification would be:
#PersistenceContext
private EntityManager entityManager;
public Role findByName(String name) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Role> cq = cb.createQuery(Role.class);
Root<Role> from = criteriaQuery.from(Role.class);
Predicate condition = criteriaBuilder.equal(from.get("name"), name);
criteriaQuery.where(condition );
Query query = entityManager.createQuery(criteriaQuery);
Role foundRole = (Role) query.getSingleResult();
return foundRole;
}
a simplier approach:
public Role findByName(String name) {
TypedQuery<Role>query= entityManager.createQuery("Select r from Role r where name=:name",MyEntity.class);
query.
return query.setParameter("name", name).getSingleResult()
}
Here you are another approach (cleaner from my point of view) to implement data access using jpa:
https://www.concretepage.com/java/jpa/jpa-entitymanager-and-entitymanagerfactory-example-using-hibernate-with-persist-find-contains-detach-merge-and-remove
I have 3 tables, User, Role, and User_role. User has a OneToMany relationship mapped by "user" with a CascadeType.Merge and user_role has 2 ManyToOne Relationships with cascadeTypes.All however the user_table never populates with data when running hibernate. Instead values are only populated in the user and role tables, but never the user_role table.
User Table Definition
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name="Id", nullable=false, updatable = false)
private Long id;
private String username;
private String password;
private String firstName;
private String lastName;
private String email;
private String phone;
private boolean enabled = true;
#OneToMany(mappedBy = "user", cascade=CascadeType.MERGE, fetch =
FetchType.EAGER)
#JsonIgnore
private Set<UserRole> userRoles = new HashSet<>();
UserRole Table:
#Entity
#Table(name="user_role")
public class UserRole implements Serializable {
private static final long serialVersionUID = 890345L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long userRoleId;
#ManyToOne(fetch = FetchType.EAGER, cascade=CascadeType.ALL,
optional=false)
#JoinColumn(name = "user_id")
private User user;
#ManyToOne(fetch = FetchType.EAGER, cascade=CascadeType.ALL,
optional=false)
#JoinColumn(name = "role_id")
private Role role;
public UserRole () {}
public UserRole (User user, Role role) {
this.user = user;
this.role = role;
}
public long getUserRoleId() {
return userRoleId;
}
public void setUserRoleId(long userRoleId) {
this.userRoleId = userRoleId;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
}
Call To userRepository.save() in userServiceImpl that is called from a commandLine Runner.
#Service
public class UserServiceImpl implements UserService {
private static final Logger LOG = LoggerFactory.getLogger(UserSecurityService.class);
#Autowired
private UserRepository userRepository;
#Autowired
private RoleRepository roleRepository;
// Indicates a Database Transaction
#Transactional
public User createUser(User user, Set<UserRole> userRoles) {
User localUser = userRepository.findByUsername(user.getUsername());
if(localUser != null) {
LOG.info("User with username {} already exist. Nothing will be done. ", user.getUsername());
} else {
for (UserRole ur : userRoles) {
roleRepository.save(ur.getRole());
}
Set<UserRole> currentRoles =user.getUserRoles();
currentRoles.addAll(userRoles);
user.setUserRoles(currentRoles);
localUser = userRepository.save(user);
}
return localUser;
}
}
Main Class Run()
public void run(String... args) throws Exception {
User user1 = new User();
user1.setFirstName("John");
user1.setLastName("Adams");
user1.setUsername("j");
user1.setPassword(SecurityUtility.passwordEncoder().encode("p"));
user1.setEmail("JAdams#gmail.com");
Set<UserRole> userRoles = new HashSet<>();
Role role1 = new Role();
role1.setRoleId(1);
role1.setName("ROLE_USER");
userRoles.add(new UserRole(user1, role1));
userService.createUser(user1, userRoles);
}
CascadeType.MERGE will only cascade merge events. Persist events wont cascade so if you try to save a new user, the persist event will not cascade to User_role and no entries will be inserted into the user_role table.
Try to add the CascadeType.PERSIST or change to CascadeType.ALL in order to cascade save the record in the database.
#OneToMany(mappedBy = "user", cascade={CascadeType.MERGE, CascadeType.PERSIST}, fetch = FetchType.EAGER)
You can find out more about Cascade events in this answer: What do REFRESH and MERGE mean in terms of databases?
I was able to persist the data to the user_role associative table by implementing the JPA entity manager. A new EntityManager class was instantiated and the data was persisted through the javax.persistence .merge() method.
I have a Spring project that uses JPA with Hibernate and MySQL database. Database includes three tables: Users, Roles, and many-to-many join table UserRoles.
Below are the corresponding classes for the tables.
ApplicationUser.java
#Entity
#Table(name = "APPLICATION_USER")
public class ApplicationUser extends ExtAuditInfo {
public static final Long SYSTEM_USERID = 1000L;
#Id
#Column(name = "APPLICATION_USER_ID")
private long applicationUserId;
#Column(name = "LOGIN_NAME")
private String loginName;
#Column(name = "LAST_NAME")
private String lastName;
#Column(name = "FIRST_NAME")
private String firstName;
#Column(name = "MIDDLE_NAME")
private String middleName;
#OneToMany(mappedBy = "id.applicationUser", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private List<UserRole> roles =new ArrayList<>();
//get and set methods
Role.java
#Entity
#Table(name = "ROLE")
#NamedQueries({
#NamedQuery(name = "Role.getRoleById", query = "select r from Role r where r.roleId =:roleId"))}
public class Role extends AuditInfo {
#Id
#Column(name="ROLE_ID")
private long roleId;
#Column(name="ACTIVE_FLAG")
private String activeFlag;
#Column(name="ROLE_NAME")
private String roleName;
#OneToMany(mappedBy = "id.role", fetch = FetchType.LAZY)
private List<UserRole> users = new ArrayList<>();
//get and set methods
UserRole.java
#Entity
#AssociationOverrides({
#AssociationOverride(name = "id.applicationUser",
joinColumns = #JoinColumn(name = "APPLICATION_USER_ID")),
#AssociationOverride(name = "id.role",
joinColumns = #JoinColumn(name = "ROLE_ID")) })
#Table(name = "USER_ROLE")
public class UserRole extends ExtAuditInfo implements Serializable{
#EmbeddedId
private UserRoleID id = new UserRoleID();
#Column(name="USER_ROLE_VER")
private long userRoleVer;
public UserRole(){
}
#Transient
public ApplicationUser getApplicationUser() {
return this.id.getApplicationUser();
}
public void setApplicationUser(ApplicationUser applicationUser) {
this.id.setApplicationUser(applicationUser);
}
public long getUserRoleVer() {
return this.userRoleVer;
}
public void setUserRoleVer(long userRoleVer) {
this.userRoleVer = userRoleVer;
}
#Transient
public Role getRole() { return this.id.getRole(); }
public void setRole(Role role) { this.id.setRole(role); }
}
UserRoleID.java
#Embeddable
public class UserRoleID implements Serializable {
#ManyToOne(cascade = CascadeType.ALL)
private ApplicationUser applicationUser;
#ManyToOne(cascade = CascadeType.ALL)
private Role role;
private static final long serialVersionUID = 1L;
public UserRoleID() {
}
public ApplicationUser getApplicationUser() {
return this.applicationUser;
}
public void setApplicationUser(ApplicationUser applicationUser) {
this.applicationUser = applicationUser;
}
public Role getRole() {
return this.role;
}
public void setRole(Role role) {
this.role = role;
}
}
Now, when I create a sample user with viewer role, the record is being inserted into the Application_user and User_Role tables, but when I try to update the role of the user it is adding a new role to the user instead of updating the existing role.
This is what I'm doing
#TransactionAttribute(TransactionAttributeType.REQUIRED)
public void updateRole(ApplicationUser appUser, long roleId){
EntityManager em=getEntityManager();
TypedQuery<Role> query = em.createNamedQuery("Role.getRoleById", Role.class);
query.setParameter("roleId",roleId);
Role r = query.getSingleResult();
UserRole userRole= appUser.getRole().get(0);
userRole.setRole(r);
em.merge(userRole);
em.flush();
em.detach(userRole);
}
Any idea, what to do to update the existing role instead of creating a new role in user_role table?
You are assigning new role to user, so a new record is added in user_role table, and old user_role entry is deleted. That's the right behavior.
So it's not you called "update the role of user".
Update:
You should delete role manually when many-to-many relationship.
appUser.getRoles().remove(userRole);
em.remove(userRole);
UserRole newUserRole = new UserRole();
newUserRole.setRole(r);
appUser.getRoles().add(newUserRole);