Closed toop closed 2 years ago
Hello, @toop.
Thanks for evaluating SAPL. The scenario you describe is typical but not trivial to implement.
The method I would recommend is to use an obligation for filtering the books together a repository implementation that has a customizable filter argument.
First, lets late a look at the book repository:
@Repository
public interface BookRepository extends CrudRepository<Book, Long> {
@PreEnforce
@Query("select b from Book b where :filter is null or b.category in :filter")
List<Book> findAll(@Param("filter") Optional<Collection<Integer>> filter);
}
Here, the findAll
method takes an optional filter parameter that can be controlled via the policies.
The REST controller calls the repository as follows:
@RestController
@RequiredArgsConstructor
public class BookController {
private final BookRepository repository;
@GetMapping("/")
List<Book> findAll() {
return repository.findAll(Optional.empty());
}
}
Now, based on this and given that you have defined a custom Principal
class containing the dataScope
you can write a matching SAPL policy set:
set "List and filter books - query modification with PreEnforce Example"
first-applicable
for action.java.name == "findAll"
policy "deny if scope null"
deny
where
subject.principal.dataScope in [null, undefined];
policy "empty scope means no limit"
permit
where
subject.principal.dataScope == [];
policy "enforce filtering"
permit
obligation {
"limitCategoriesTo" : subject.principal.dataScope
}
The first policy in the set denies access if the data scope is null or not defined.
The second policy grants access without any obligations if the scope is an empty array (your admin case).
The final policy grants access, with an obligation to limit the categories to those listed in the scope.
By default, the Spring integration of SAPL cannot know the semantics of obligations applying to any domain. So you now have to provide a constraint handler provider that can correctly enforce the given obligation. In this case you have to change the filter parameter of the query method. Thus, implement a MethodInvocationConstraintHandlerProvider
:
@Service
public class EnforceCategoryFilteringConstraintHandlerProvider implements MethodInvocationConstraintHandlerProvider {
private static final String LIMIT_CATEGORIES = "limitCategoriesTo";
@Override
public boolean isResponsible(JsonNode constraint) {
return constraint.has(LIMIT_CATEGORIES) && constraint.get(LIMIT_CATEGORIES).isArray();
}
@Override
public Consumer<ReflectiveMethodInvocation> getHandler(JsonNode constraint) {
return methodInvocation -> {
var constraintCategories = constraint.get(LIMIT_CATEGORIES);
var categories = new ArrayList<Integer>();
if (constraintCategories.size() == 0) {
var newArguments = new Object[] { Optional.empty() };
methodInvocation.setArguments(newArguments);
return;
}
for (var category : constraintCategories)
categories.add(category.asInt());
var newArguments = new Object[] { Optional.of(categories) };
methodInvocation.setArguments(newArguments);
};
}
}
This handler will change the parameter of the method accordingly if the decision contains the respective obligation.
I added a quick demo of your scenario with the data you provided above: https://github.com/heutelbeck/sapl-demos/tree/master/sapl-demo-books
Using OAuth is not really a SAPL-specific issue. There is however an OAuth/JWT demo available as well, where the contents of the JWT token are used in policies: https://github.com/heutelbeck/sapl-demos/tree/master/sapl-demo-jwt
Please let me know if this helps. Also, we have a Discord for discussion and support: https://discord.gg/pRXEVWm3xM I would be interested in which context you are using SAPL.
Dominic
There are also other ways of achieving the result. I.e., instead of manipulating the query via the method parameters, you could implement a constraint handler that filters the complete list. However, that would be pretty wasteful with more extensive collections. I would prefer the variant shown above.
Thank you very much for your reply and solution. Maybe are not familiar with SAPL and feel it is difficult to implement it. I will get familiar with SAPL as soon as possible, and then test it. In addition to SQL filtering query, is there any other processing method of conditional filtering? Please recommend or sample, thank you again!
The alternative is to make the decision beforehand, perform the query to retrieve the entire collection without a filter and then apply a filter before handing the data off to the client class.
Currently, this behavior is supported for reactive data types, i.e., Flux<?>
.
The demos contain an example of that:
This is a scenario similar to your book example but is based on a Bell-LaPadula security model with classified documents.
At this moment, filtering is not supported for collection-type return types out-of-the-box. However, we plan to add this feature soon.
Also, there is the possibility to include the complete dataset as a resource in the authorization subscription and to filter inside of the policies using the transform`` feature of policies. This, however, is not recommended for non-trivial-size collections and is more feasible for individual data entries. This can be done using the
@PostEnforce``` annotation. Example:
Finally, a little bit further down the road, we pan to support generic SQL/MongoDB Spring Data policy enforcement points without the need to add filter arguments to repository classes. But this will undoubtedly take a few months to be completed.
I just double-checked. In fact, filtering with @PreEnforce
for collections is impossible due to how Spring Security is implemented.
But, with @PostEnforce
collection filtering based on a constraint is actually possible right out of the box. You can use a MappingConstraintHandlerProvider<T>
to implement the type of filtering you require for non-reactive return types.
On the springboot, when a user accesses /books/list, I need to filter books data according to the data_scope range data of the currently logged in user and the category value of books. Please ask how to do this. Please give some collective examples. Thank you very much!
USER:
user | dept | data_scope admin | 1 | [,] Tom | 1 | [1,2,3] Sim | 2 | [1,2] Kat | 3 | null
BOOKS:
id | name | category 1 | book1 | 1 2 | book2 | 1 3 | Book3 | 2 4 | book4 | 3 5 | book5 | 4 6 | book6 | 5
when user admin to access the api /books/list, can visible all data,return the data:
id | name | category 1 | book1 | 1 2 | book2 | 1 3 | Book3 | 2 4 | book4 | 3 5 | book5 | 4 6 | book6 | 5
when user Tom to access the api /books/list, books.category in user.data_scope, return the data: id | name | category 1 | book1 | 1 2 | book2 | 1 3 | Book3 | 2 4 | book4 | 3
when user Sim to access the api /books/list, books.category in user.data_scope, return the data: id | name | category 1 | book1 | 1 2 | book2 | 1 3 | Book3 | 2
when user Kat to access the api /books/list, user.data_scope is null, return the exception: “access denied,missing permissions”
How to implement the above requirements in MVC and oauth2 environment? Please give some practical examples. Thank you very much!