This Java library provides functionality for managing access control based on permissions and restrictions on entities, along with custom JPA repository implementations to handle advanced query lookup strategies.
-
Access Control Management:
- Provides functionality to restrict access to entities based on entity ID, creator, membership, handler and composed restriction.
- Flexible support for combining multiple restrictions (
AND,ORconditions).
-
Custom JPA Repository:
- Leverages Spring Data JPA with custom repository implementations.
- Supports dynamic query building with Criteria API and customizable query lookup strategies.
- Modular repository interfaces for different query types.
-
JSON Serialization/Deserialization:
- Built-in support for serializing and deserializing restrictions using Jackson.
- Easily store and retrieve access control rules in JSON format.
- Java 21+
- Spring Boot 3+ or any Spring-based environment
- Maven or Gradle (for dependency management)
- Hibernate 6.x (or higher) for ORM functionality
- Jackson for JSON serialization/deserialization (optional)
To use this library, add the following dependency to your pom.xml:
<dependency>
<groupId>com.lprevidente</groupId>
<artifactId>permissio</artifactId>
<version>0.0.1</version>
</dependency>Enable JPA Repositories:
Ensure that your Spring Boot application has the necessary configuration to enable JPA repositories.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication
@EnableAcRepositories(basePackages = "com.example.repositories")
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}The library provides a modular approach with specialized repository interfaces that you can use based on your needs:
The main repository interface that extends all other interfaces, providing comprehensive access control functionality. Use this when you need all features.
public interface UserRepository extends AcRepository<User, Long> {
// Custom methods
}Specialized interface for repositories that need to execute JPA Specifications with access control criteria.
public interface ProductRepository
extends JpaRepository<Product, Long>, AcRepositorySpecificationExecutor<Product, Long> {
// Using specifications
default List<Product> findActiveProductsByCategory(String category, Requester requester) {
AcCriteria criteria = AcCriteria.builder().request(requester).build();
Specification<Product> spec =
(root, query, cb) ->
cb.and(cb.equal(root.get("category"), category), cb.equal(root.get("active"), true));
return findAll(criteria, spec);
}
}Specialized interface for repositories that need to work with related entities and entity graphs.
public interface TeamRepository
extends JpaRepository<Team, Long>, AcRepositoryRelated<Team, Long> {}The library provides various restriction types to control access to entities:
| Restriction | Description |
|---|---|
ById |
Restricts access to an entity based on a specific ID property. |
ByCreator |
Restricts access to entities created by the current requester. |
ByMember |
Restricts access to entities where the requester is a member. |
ByHandler |
Restricts access based on handlers with specific type and ID. |
ByRelatedEntity |
Restricts access based on a related entity's properties. |
And |
Combines multiple restrictions with AND logic. |
Or |
Combines multiple restrictions with OR logic. |
Conjunction |
Represents a specialized conjunction (all conditions must be true). |
Disjunction |
Represents a specialized disjunction (at least one condition must be true). |
Restricts access to entities with a specific property value.
new ById("id",1); // Access entity with id = 1Restricts access to entities created by the current requester.
new ByCreator("creator.id"); // Access entities where creator.id equals requester's ID
new ByCreator("creatorId"); // Access entities where creatorId equals requester's IDRestricts access to entities where the requester is a member.
new ByMember("members.id"); // For one-to-many relationships
new ByMember("members.user.id"); // For custom join relationshipsRestricts access based on handlers with specific type and ID.
// Any handler type where handler.id matches the requester's ID
new ByHandler(
new Type("*", "handlers.type"),
new Id("id","handlers.handler.id")
);
// Only HR type handlers where handler.id matches the requester's ID
new ByHandler(
new Type("HR", "handlers.type"),
new Id("id","handlers.id")
);Restricts access based on properties of a related entity.
// Access users related to office with ID 1
new ByRelatedEntity("office",new ById("id", 1L));
// Access users who are members of teams with ID 1
new ByRelatedEntity("teams.team",new ById("id", 1L));Combine multiple restrictions with logical operators.
// Access entities that match both restrictions
new And(new ById("id", 2L), new ByCreator("creator.id"));
// Access entities that match either restriction
new Or(new ById("id", 1L), new ByCreator("creator.id"));All restrictions can be serialized to and deserialized from JSON, making it easy to store and load access control rules.
Jackson is used for JSON processing with type information included in the @type property.
{
"@type": "byId",
"property": "id",
"id": 1
}{
"@type": "byCreator",
"property": "creator.id"
}{
"@type": "byMember",
"property": "members.id"
}{
"@type": "byHandler",
"type": {
"type": "*",
"property": "handlers.handler"
},
"id": {
"field": "id",
"property": "handlers.handler"
}
}{
"@type": "and",
"restrictions": [
{
"@type": "byId",
"property": "id",
"id": 1
},
{
"@type": "byCreator",
"property": "creator.id"
}
]
}{
"@type": "byRelatedEntity",
"property": "team",
"restriction": {
"@type": "byId",
"property": "id",
"id": 1
}
}// Create a restriction
Restriction restriction = new ByRelatedEntity("team", new ById("id", 1));
// Serialize to JSON
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(restriction);
// Deserialize from JSON
Restriction deserializedRestriction = mapper.readValue(json, Restriction.class);Define a requester with permissions and restrictions:
// Create a requester with ID 1 who can read users they created
Requester<Long> requester = Requester.builder()
.id(1L)
.addPermission("user:read", new ByCreator("creator.id"))
.build();
// Build criteria with the requester and permission
AcCriteria criteria = AcCriteria.builder()
.request(requester)
.permission("user:read")
.build();
// Use the criteria with your repository
List<User> users = userRepository.findAll(criteria);// Create a restriction that allows access to entity with ID 2 only if the requester is also the creator
Restriction restriction = new And(
new ById("id", 2L),
new ByCreator("creator.id")
);
// Apply the restriction to a specific permission
Requester<Long> requester = new Requester<>(1L, Map.of("user:read", restriction));
// Use in a criteria query
AcCriteria criteria = AcCriteria.builder()
.request(requester)
.permission("user:read")
.build();
// Execute the query
List<User> users = userRepository.findAll(criteria);// Access users who belong to teams with ID 1 or 2
Restriction restriction = new ByRelatedEntity(
"teams.team",
new Or(
new ById("id", 1L),
new ById("id", 2L)
)
);
// Apply restriction to the "user:read" permission
Requester<Long> requester = new Requester<>(1L, Map.of("user:read", restriction));
// Build and execute the query
AcCriteria criteria = AcCriteria.builder()
.request(requester)
.permission("user:read")
.build();
List<User> users = userRepository.findAll(criteria);See the examples in the test package for more detailed usage scenarios.
You can create your own custom restrictions by implementing the Restriction interface and registering the subtype with
Jackson's ObjectMapper.
-
Implement the
RestrictionInterface:package com.lprevidente.permissio.restriction; import com.lprevidente.permissio.entity.Requester; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Predicate; import java.util.Map; public record CustomRestriction() implements Restriction<Requester> { @Override public Predicate toPredicate( Requester requester, Path<?> path, CriteriaBuilder cb, Map<String, Join> joinMap) { // Your custom logic here return cb.equal(path.get("yourProperty"), requester.getId()); } }
-
Register the Subtype with ObjectMapper:
Ensure that your custom restriction is recognized by Jackson's ObjectMapper:import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.jsontype.NamedType; import com.lprevidente.permissio.restriction.CustomRestriction; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration class JacksonConfig { @Bean ObjectMapper objectMapper() { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.registerSubtypes(new NamedType(CustomRestriction.class, "customRestriction")); return objectMapper; } }
By following these steps, you can extend the restriction functionality and ensure that your custom restrictions are properly serialized and deserialized by Jackson.
This project is licensed under the MIT License - see the LICENSE file for details.