6.2.5.1. Constraints

Constraints allow you to configure row-level access control, i.e. to manage access to particular rows of data. Unlike permissions which affect the whole types of entities, constraints affect particular entity instances. Constraints can be set for reading, creating, updating and deletion, so the framework will filter out some entities when loading or disable entity operations for an instance if it matches a constraint. Besides, one can add custom constraints not related to CRUD actions.

A user gets the set of constraints from all groups starting with their own one, and up the hierarchy. Therefore, the lower the users are in the groups hierarchy, the more constraints they have.

Note that constraints are checked for all operations performed from the client tier through the standard DataManager. If an entity does not match the constraints conditions during creation, modification or deletion, the RowLevelSecurityException is thrown. See also the Data Access Checks section for how security constraints are used by different mechanisms of the framework.

There are two types of constraints: checked in database and checked in memory.

  1. For the constraints checked in database, conditions are specified using JPQL expression fragments. These fragments are appended to all entity selection queries, so the entities not matching the conditions are filtered on the database level. Constraints checked in database can be used only for the read operation and affect only root entities of the loaded object graphs.

  2. For the constraints checked in memory, the conditions are specified using Java code (if the constraint is defined at design time) or Groovy expressions (if the constraint is defined at run time). The expressions are executed for every entity in the checked graph of objects, and if the entity does not match the conditions, it is filtered out from the graph.

Defining constraints at design time

Constraints can be defined in a class extending AnnotatedAccessGroupDefinition that is used to define the access group. The class must be located in the core module. Below is an example of an access group that defines a few constraints for the Customer and Order entities:

@AccessGroup(name = "Sales", parent = RootGroup.class)
public class SalesGroup extends AnnotatedAccessGroupDefinition {

    @JpqlConstraint(target = Customer.class, where = "{E}.grade = 'B'") (1)
    @JpqlConstraint(target = Order.class, where = "{E}.customer.grade = 'B'") (2)
    @Override
    public ConstraintsContainer accessConstraints() {
        return super.accessConstraints();
    }

    @Constraint(operations = {EntityOp.CREATE, EntityOp.READ, EntityOp.UPDATE, EntityOp.DELETE}) (3)
    public boolean customerConstraints(Customer customer) {
        return Grade.BRONZE.equals(customer.getGrade());
    }

    @Constraint(operations = {EntityOp.CREATE, EntityOp.READ, EntityOp.UPDATE, EntityOp.DELETE}) (4)
    public boolean orderConstraints(Order order) {
        return order.getCustomer() != null && Grade.BRONZE.equals(order.getCustomer().getGrade());
    }

    @Constraint(operations = {EntityOp.UPDATE, EntityOp.DELETE}) (5)
    public boolean orderUpdateConstraints(Order order) {
        return order.getAmount().compareTo(new BigDecimal(100)) < 1;
    }
}
1 - load only customers with grade attribute equals B (corresponds to the Grade.BRONZE enum value).
2 - load only orders for customers with grade attribute equals B.
3 - in-memory constraint that filters out customers with grade other than Grade.BRONZE from loaded object graphs.
4 - in-memory constraint that allows to work with orders for customers only with grade == Grade.BRONZE.
5 - in-memory constraint that allows to modify or delete only orders with amount < 100.

Consider the following rules when writing JPQL constraints:

  • The {E} string should be used as an alias of the entity being loaded. On execution of the query, it will be replaced with a real alias specified in the query.

  • The following predefined constants can be used in JPQL parameters:

    • session$userLogin – login name of the current user (in case of substitution – the login name of the substituted user).

    • session$userId – ID of the current user (in case of substitution – ID of the substituted user).

    • session$userGroupId – group ID of the current user (in case of substitution − group ID of the substituted user).

    • session$XYZ – arbitrary attribute of the current user session, where XYZ is the attribute name.

  • The where attribute value is added to the where query clause using and condition. Adding where word is not needed, as it will be added automatically.

  • The join attribute value is added to the from query clause. It should begin with a comma, join or left join.

Defining constraints at run time

In order to create a constraint, open the Access Groups screen, select a group to create the constraint for, and go to the Constraints tab. The constraint edit screen contains the Constraint Wizard which helps to construct simple JPQL and Groovy expressions with entity attributes. When you select Custom as an operation type, the required Code field appears and you should set a code which will be used to identify the constraint.

The JPQL editor in the Join Clause and Where Clause fields supports auto-completion for entity names and their attributes. In order to invoke auto-completion, press Ctrl+Space. If the invocation is made after the period symbol, an entity attributes list matching the context will be shown, otherwise – a list of all data model entities.

In in-memory Groovy constraints, use {E} placeholder as a variable containing the checked entity instance. Besides, the userSession variable of the UserSession type is passed to the script. The following example shows a constraint checking that the entity is created by the current user:

{E}.createdBy == userSession.user.login

When a constraint is violated, a notification is shown to the user. Notification caption and message for each constraint can be localized: see Localization button on the Constraints tab of the Access Groups screen.

Checking constraints in application code

A developer can check the constraints conditions for the particular entity using the following methods of the Security interface:

  • isPermitted(Entity, ConstraintOperationType) - to check constraints by the operation type.

  • isPermitted(Entity, String) - to check custom constraints by the constraint code.

Also, it is possible to link any any action based on the ItemTrackingAction class with a certain constraint. The constraintOperationType attribute should be set for the action XML element or using the setConstraintOperationType() method. Be aware that the constraint code will be executed on the client tier, so it must not use middleware classes.

Example:

<table>
    ...
    <actions>
        <action id="create"/>
        <action id="edit" constraintOperationType="update"/>
        <action id="remove" constraintOperationType="delete"/>
    </actions>
</table>