I'm looking for a way to authorize access based on ROLES and DATA CONTENT (table row) using Spring Boot & JPA
The use case is the following:
Given a User with a Role 'ROLE_AAA'
When user fetches data from table 'REPORT'
Then only reports with specific 'REPORT_ROLE' association will be returned
ER
Table: role
ID
ROLE
1
ROLE_AAA
2
ROLE_BBB
Table: report
ID
CONTENT
1
...
2
...
3
...
4
...
5
...
Table: report_roles
REPORT_ID
ROLE_ID
1
1
2
1
3
2
4
2
5
1
In this case a user with role ROLE_AAA will get reports with ID 1,2 & 5 only.
SELECT re.*
FROM report re
JOIN report_role rr
ON re.id = rr.report_id
JOIN role ro
ON ro.id = rr.role_id
WHERE ro.name = 'ROLE_AAA'
I could easily write queries (#org.springframework.data.jpa.repository.Query) joining on table I want to get data from, with the association role table, but I feel I'm missing a JPA or Spring Security feature that might offer it out off the box.
Any design/implementation suggestion will be very appreciated.
UPDATE
Since returns will be paginated (retuning Page<Object> from JPARepository), I believe PostFiltering is not an option here.
Here is an older article I wrote on the subject of using #Pre/Post annotations with a custom PermissionEvaluator. It seems your best bet here, and allows you to purely use annotations instead of custom queries. Of course, it only works if there aren't too many reports being returned from your query (without a ro.name = 'ROLE_AAA' filter).
In your case however, you aren't really trying to use the hasPermission(filterObject, 'xyz') syntax because your use case is role-based, meaning permissions are roles and are already in the authentication object (e.g. authorities) and the database (e.g. report_roles). With a M:N relationship between user roles and report roles, you can implement a helper to do the check for you, like this:
#RestController
public class ReportsController {
#GetMapping("/reports")
#PostFilter("#reportExpressions.hasAnyRole(filterObject, authentication)")
public List<Report> getReports() {
return new ArrayList<>(List.of(
new Report("Report #1", Set.of("ROLE_AAA", "ROLE_BBB")),
new Report("Report #2", Set.of("ROLE_AAA", "ROLE_CCC"))
));
}
#Component("reportExpressions")
public static class ReportExpressions {
public boolean hasAnyRole(Report report, Authentication authentication) {
Set<String> authorities = AuthorityUtils.authorityListToSet(
authentication.getAuthorities());
return report.getRoles().stream().anyMatch(authorities::contains);
}
}
public static class Report {
private final String name;
private final Set<String> roles;
public Report(String name, Set<String> roles) {
this.name = name;
this.roles = roles;
}
public String getName() {
return name;
}
public Set<String> getRoles() {
return roles;
}
}
}
If the current user has ROLE_AAA, they will see both reports, etc. My example uses a controller, but you can apply the same annotation to a JPA repository.
Related
My Java Spring application, which uses ACLs, has a service method to retrieve all the objects corresponding to a given user:
#Override
#PostFilter("hasPermission(filterObject, 'READ') or hasPermission(filterObject, 'ADMINISTRATION')")
public List<SomeClass> findAll() {
return SomeClassRepository.findAll();
}
Unfortunately, where there are many objects in the database, this method takes too much time to complete (over 1 second). Probably because it will first fetch all the objects from the database, and then filter them one by one in memory. How can I optimize this without losing the benefits of Spring ACLs?
Edit: the solution I came up with for now is to create repositories for the acl_sid and acl_entry repositories and to fetch the IDs of the object of interest through those repositories. This gives me a 10x improvement in execution time compared to the method above. The new code looks like this:
#Override
#PostFilter("hasPermission(filterObject, 'READ') or hasPermission(filterObject, 'ADMINISTRATION')")
public List<SomeClass> findAll() {
List<SomeClass> result = new ArrayList<SomeClass>();
Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
AclSid sid = aclSidRepository.findBySid(Long.toString(userId));
List<AclEntry> aclEntries = aclEntryRepository.findBySid(sid);
for (AclEntry aclEntry : aclEntries) {
AclObjectIdentity aclObjectIdentity = aclEntry.getAclObjectIdentity();
AclClass aclClass = aclObjectIdentity.getObjectIdClass();
if (aclClass.getClassName().equals("com.company.app.entity.SomeClass")) {
Optional<SomeClass> SomeClass = SomeClassRepository
.findById(aclObjectIdentity.getObjectIdIdentity());
if (SomeClass.isPresent()) {
result.add(SomeClass.get());
}
}
}
return result;
}
As Spring filters the information in memory the performance will depend on the actual number of results: if there is a large amount of them, I am afraid that perhaps only caching your repository results before filtering the information may be a suitable solution.
To deal with the problem, you can filter the results at the database level. Two approaches come to my mind:
Either use Specifications, and filter the results at the database level taking into account the information about the principal exposed by Spring Security SecurityContext and including the necessary filter Predicates in order to restrict the information returned.
Or, if you are using Hibernate, use entity filters to again, based on the information about the principal exposed by Spring Security, apply the necessary data restrictions. Please, see this related SO question which provides great detail about the solution.
Please, consider for instance the use case of the Spring Data Specifications.
Instead of SomeClass, let's suppose that we are working with bank accounts. Let's create the corresponding entity:
#Entity
public class BankAccount {
#Id
private String accountNumber;
private Float balance;
private String owner;
private String accountingDepartment;
//...
}
And the corresponding repository:
public interface BankAccountRepository extends Repository<BankAccount, String>, JpaSpecificationExecutor<BankAccount, String> {
}
In order to filter the information depending on the user who is performing the query, we can define an utility method that, based on the user permissions, returns a List of Predicates that we can add later to the ones we are using in a certain Specification when filtering the bank accounts:
public static List<Predicate> getPredicatesForRestrictingDataByUser(Root<BankAccount> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
// I realized in your edit that you are returning the user id instead of the user object.
// There is nothing wrong with it but you are losing a valuable information: if you provide
// a convenient UserDetails implementation you can have direct access to the authorities a user has, etc
User user = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// Restrict data based on actual permissions
// If the user is an admin, we assume that he/she can see everything, and we will no return any predicates
if (hasAuthority(user, 'ADMINISTRATION')) {
return Collections.emptyList();
}
// Let's introduce the accounting manager role.
// Suppose that an accounting manager can see all the accounts in his/her department
if (hasAuthority(user, 'ACCOUNTING_MANAGER')) {
return Collections.singletonList(cb.equal(root.get(BankAccount_.accountingDeparment), user.getDepartment()))
}
// In any other case, a user can only see the bank account if he/she is the account owner
return Collections.singletonList(cb.equal(root.get(BankAccount_.owner), user.getId()));
}
Where hasAuthority can look like:
public static boolean hasAuthority(User user, String... authorities) {
if (user instanceof UserDetails) {
for (String authority : authorities) {
return authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.findAny(a -> a.equals(authority))
.isPresent();
}
}
return false;
}
Now, use these methods when constructing your Specifications. Consider for instance:
public static Specification<BankAccount> getBankAccounts(final BankAccountFilter filterCriteria) {
return new Specification<BankAccount>() {
#Override
public Predicate toPredicate(Root<BankAccount> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<Predicate>();
// Build your predicate list according to the user provided filter criteria
String accountNumber = filterCriteria.getAccountNumber();
if (accountNumber != null) {
predicates.add(cb.equal(root.get(BankAccount_.accountNmber), accountNumber);
}
//...
// And now, restrict the information a user can see
// Ideally, define getPredicatesForRestrictingDataByUser in a generic class more suitable for being reused
List<Predicate> predicatesForRestrictingDataByUser = getPredicatesForRestrictingDataByUser(root, query, cb);
predicates.addAll(predicatesForRestrictingDataByUser);
Predicate predicate = cb.and(predicates.toArray(new Predicate[predicates.size()]));
return predicate;
}
};
}
Please, forgive me for the simple use case, but I hope you get the idea.
The solution proposed in his comment by #OsamaAbdulRehman looks interesting as well, although I honestly never tested it.
I have User class like this :
#Data
#Entity
public class User {
#Id #GeneratedValue Long userID;
String eMail;
String passwordHash;
}
And I have data like this :
[{"userID":1,"passwordHash":"asdasd","email":"admin#admin.com"},
{"userID":2,"passwordHash":"12345","email":"admin1asdasd#admin.com"}]
I have two method in my controller class, one - to get single user :
#GetMapping("/user/{id}")
User one(#PathVariable Long id) {
return repository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
Other method to retrieve all user :
#GetMapping("/user")
List<User> all() {
return repository.findAll();
}
In my browser, going to this address - http://localhost:8080/user , I can see these data. And if I goto http://localhost:8080/user/id I can get a specific value.
Now my question is how can access data like http://localhost:8080/user/email/passwordHash? I am quite sure that it is not possible, because I haven't stored data in that way.
As my main target is to verify login, I have already written a #Query in my repository interface. Here it is :
public interface UserRepository extends JpaRepository<User, Long> {
#Query("select u from User u where u.eMail = ?1 and u.passwordHash = ?2")
List<User> listByLoginCredential(String emailAddress,String passwordHash);
}
Can Anyone tell me how can I do this,use this method of this interface?
I think you can can achieve what you want by adding the following method to the controller class:
#GetMapping("/user/{emailAddress}/{passwordHash}")
List<User> byMailAndPassword(#PathVariable String emailAddress, #PathVariable String passwordHash) {
return repository.listByLoginCredential(emailAddress, passwordHash)
}
On the other hand you say that your main goal is to verify login, so it looks like you are doing authentication. If you have time you should look into doing it with spring security https://spring.io/guides/gs/securing-web/#initial
Maybe this help https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.
Or you can also create procedure in Database and call stored procedure with Spring boot.
Login is related to security so ideally you should create a separate post method and use the repository method. Always make sure to follow the best practice.
Spring security is something you can utilize for better authentication and authorization.
I am using Dropwizard with JDBI. I have a typical dao for user data:
public interface UserDao
{
#SqlQuery("select * from users where role = :id")
#Mapper(UserMapper.class)
String findNameById(#BindBean Role role);
}
The user itself has an attribute with a Role type:
class User
{
private Role role;
/* the rest: other attributes, getters, setters, etc. */
}
Role is contained in another table called roles. Now, I need to map Role in the mapper, but I do not want to change the SELECT ... statement to add the JOIN roles ... part. We all know how joins affect queries and in the long run I'd like to avoid any joins if possible.
I know, that ResultSetMapper interface has a map() method, which gets a StatementContext passed to it. That context has a getBinding() method, which returns a Binding class with all the data I need:
named = {HashMap$Node#1230} size = 3
0 = {HashMap$Node#1234} "id" -> "1"
1 = {HashMap$Node#1235} "name" -> "TestRole"
2 = {HashMap$Node#1236} "class" -> "class com.example.Role"
But that class com.example.Role is not an instance of Role, it's an instance of Argument and I can't work with it.
So, is there a way to get that Role argument and I just don't see it or do I have to instantiate it (again...) from the binding arguments (obviously they are there as debugger shows)?
I finally solved it by using a custom binder. First, I modified UserDao to use #BindRole instead of #BindBean.
Next, I had to create the binder for the role. Here the role is bound manually on separate values:
#BindingAnnotation(BindRole.RoleBinderFactory.class)
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.PARAMETER})
public #interface BindRole
{
public static class RoleBinderFactory implements BinderFactory
{
public Binder build(Annotation annotation)
{
return new Binder<BindRole, User>()
{
public void bind(SQLStatement q, BindRole bind, Role role)
{
q.bind("id", role.getId());
q.bind("name", role.getName());
q.define("role", role);
}
};
}
}
}
Notice the define() method, it is responsible for setting attributes in StatementContext, so don't overlook it.
Next, in the mapper I just have to get the Role with getArgument():
Role role = new Role();
role.setId(1);
role.setName("TestRole");
Role r = (Role) statementContext.getAttribute("role");
boolean equals = e.equals(role);
In the debugger equals is shown as true, so problem solved. Woohoo.
So I have looked at various tutorials about JPA with Spring Data and this has been done different on many occasions and I am no quite sure what the correct approach is.
Assume there is the follwing entity:
package stackoverflowTest.dao;
import javax.persistence.*;
#Entity
#Table(name = "customers")
public class Customer {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private long id;
#Column(name = "name")
private String name;
public Customer(String name) {
this.name = name;
}
public Customer() {
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
We also have a DTO which is retrieved in the service layer and then handed to the controller/client side.
package stackoverflowTest.dto;
public class CustomerDto {
private long id;
private String name;
public CustomerDto(long id, String name) {
this.id = id;
this.name = name;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
So now assume the Customer wants to change his name in the webui - then there will be some controller action, where there will be the updated DTO with the old ID and the new name.
Now I have to save this updated DTO to the database.
Unluckily currently there is no way to update an existing customer (except than deleting the entry in the DB and creating a new Cusomter with a new auto-generated id)
However as this is not feasible (especially considering such an entity could have hundreds of relations potentially) - so there come 2 straight forward solutions to my mind:
make a setter for the id in the Customer class - and thus allow setting of the id and then save the Customer object via the corresponding repository.
or
add the id field to the constructor and whenever you want to update a customer you always create a new object with the old id, but the new values for the other fields (in this case only the name)
So my question is wether there is a general rule how to do this?
Any maybe what the drawbacks of the 2 methods I explained are?
Even better then #Tanjim Rahman answer you can using Spring Data JPA use the method T getOne(ID id)
Customer customerToUpdate = customerRepository.getOne(id);
customerToUpdate.setName(customerDto.getName);
customerRepository.save(customerToUpdate);
Is's better because getOne(ID id) gets you only a reference (proxy) object and does not fetch it from the DB. On this reference you can set what you want and on save() it will do just an SQL UPDATE statement like you expect it. In comparsion when you call find() like in #Tanjim Rahmans answer spring data JPA will do an SQL SELECT to physically fetch the entity from the DB, which you dont need, when you are just updating.
In Spring Data you simply define an update query if you have the ID
#Repository
public interface CustomerRepository extends JpaRepository<Customer , Long> {
#Query("update Customer c set c.name = :name WHERE c.id = :customerId")
void setCustomerName(#Param("customerId") Long id, #Param("name") String name);
}
Some solutions claim to use Spring data and do JPA oldschool (even in a manner with lost updates) instead.
Simple JPA update..
Customer customer = em.find(id, Customer.class); //Consider em as JPA EntityManager
customer.setName(customerDto.getName);
em.merge(customer);
This is more an object initialzation question more than a jpa question, both methods work and you can have both of them at the same time , usually if the data member value is ready before the instantiation you use the constructor parameters, if this value could be updated after the instantiation you should have a setter.
If you need to work with DTOs rather than entities directly then you should retrieve the existing Customer instance and map the updated fields from the DTO to that.
Customer entity = //load from DB
//map fields from DTO to entity
So now assume the Customer wants to change his name in the webui -
then there will be some controller action, where there will be the
updated DTO with the old ID and the new name.
Normally, you have the following workflow:
User requests his data from server and obtains them in UI;
User corrects his data and sends it back to server with already present ID;
On server you obtain DTO with updated data by user, find it in DB by ID (otherwise throw exception) and transform DTO -> Entity with all given data, foreign keys, etc...
Then you just merge it, or if using Spring Data invoke save(), which in turn will merge it (see this thread);
P.S. This operation will inevitably issue 2 queries: select and update. Again, 2 queries, even if you wanna update a single field. However, if you utilize Hibernate's proprietary #DynamicUpdate annotation on top of entity class, it will help you not to include into update statement all the fields, but only those that actually changed.
P.S. If you do not wanna pay for first select statement and prefer to use Spring Data's #Modifying query, be prepared to lose L2C cache region related to modifiable entity; even worse situation with native update queries (see this thread) and also of course be prepared to write those queries manually, test them and support them in the future.
I have encountered this issue!
Luckily, I determine 2 ways and understand some things but the rest is not clear.
Hope someone discuss or support if you know.
Use RepositoryExtendJPA.save(entity). Example:
List<Person> person = this.PersonRepository.findById(0)
person.setName("Neo");
This.PersonReository.save(person);
this block code updated new name for record which has id = 0;
Use #Transactional from javax or spring framework. Let put #Transactional upon your class or specified function, both are ok. I read at somewhere that this annotation do a "commit" action at the end your function flow. So, every things you modified at entity would be updated to database.
There is a method in JpaRepository
getOne
It is deprecated at the moment in favor of
getById
So correct approach would be
Customer customerToUpdate = customerRepository.getById(id);
customerToUpdate.setName(customerDto.getName);
customerRepository.save(customerToUpdate);
I want to implement relationships from JPA to Spring JDBC. For instance, assume I have Account and Advert objects. The relationship between Account and Advert is #OneToMany according to JPA.
Account class:
public class Account {
private Long id;
private String username;
private Set<Advert> adverts = new HashSet<Advert>();
// getters + setters
}
Advert class:
public class Advert {
private Long id;
private String text;
private Account account;
// getters + setters
}
AccountMapper:
public class AccountMapper implements RowMapper<Account> {
public Account mapRow(ResultSet rs, int rowNum) throws SQLException {
Account account = new Account();
account.setId(rs.getLong("id"));
account.setUsername(rs.getString("username"));
return account;
}
}
Now, I am trying to create a Mapper for the Advert class. How can I map the account variable from the Advert class to a row? Many thanks
You can use Hibernate without affecting your application performance, just check out this Hibernate tutorial for hundreds of examples related too mapping entities.
As for doing that in JDBC, you need to doo the following steps:
You need to use aliases to all selected columns so that the ids columns won't clash.
You can define two row mappers and use a join from Advert to Account and pass it to the AccountMapper:
public class AdvertMapper implements RowMapper<Advert> {
public Advert mapRow(ResultSet rs, int rowNum) throws SQLException {
Advert advert = new Advert();
advert.setId(rs.getLong("advert_id"));
advert.setText(rs.getString("advert_text"));
return advert;
}
}
public class AccountMapper implements RowMapper<Account> {
private final AdvertMapper advertMapper;
public AccountMapper(AdvertMapper advertMapper) {
this.advertMapper = advertMapper;
}
public Account mapRow(ResultSet rs, int rowNum) throws SQLException {
Account account = new Account();
account.setId(rs.getLong("account_id"));
account.setUsername(rs.getString("account_username"));
Advert advert = this.advertMapper.mapRow(rs, rowNum);
advert.setAccount(account);
account.getAdverts().add(advert);
return account;
}
}
The AccountMapper uses the AdvertMapper to create Adverts from the joined data.
Compare this to Hibernate, where all these mappings are resolved for you.
Well if you do not use an ORM ... you have no object relation mapping ! After all the ORMs were created for that reason :-)
More seriously, ORM saves you from writing a lot of boilerplate code. Using direct JDBC instead of JPA is a code optimisation. Like any other code optimisation, it should be used when appropriate. It is relevant for :
libraries using few tables that do not want to rely on an ORM (ex: user, roles, and ACL in spring security)
identified bottlenecks in larger application
My advice should be to first use JPA or native hibernate hidden in a DAO layer. Then carefully analyze your performance problems and rewrite the most expensive parts in JDBC.
Of course, you can directly code you DAO implementations in JDBC, but it will be much longer to write.
I almost forgot the essential part : in an ORM you map classes and relations, in JDBC you write independant SQL queries.
Solving the one to one case is easy with as Vlad answered, If you want to map a one to many as your Account - Advert suggest you can't do that
with a RowMapper because you will try to map multiple rows of your ResultSet to one Account, many Advert.
You can also do that manually or you can also use http://simpleflatmapper.org that provides mapping from ResultSet to POJO with one to many support.
Beware that the bidirectional relationship is not great there if you really want those it's possible but they won't be the same instance.
Checkout
http://simpleflatmapper.org/0104-getting-started-springjdbc.html
and
https://arnaudroger.github.io/blog/2017/02/27/jooq-one-to-many.html
you will need to get a ResutlSetExtractor - it's thread safe so only need one instance -,
private final ResultSetExtractor<List<Account>> mapper =
JdbcTemplateMapperFactory
.newInstance()
.addKeys("id") // assuming the account id will be on that column
.newResultSetExtractor(Account.class);
// in the method
String query =
"SELECT ac.id as id, ac.username, ad.id as adverts_id, ad.text as adverts_text"
+ "FROM account ac LEFT OUTER JOIN advert ad ON ad.account_id = ac.id order by id "
// the order by id is important here as it uses the break on id on the root object
// to detect new root object creation
List<Account> results = template.query(query, mapper);
with that you should get a list of account with the list of adverts populated. but advert won't have the account.