Security constraints in CUBA
CUBA Security Inc. is still around and just recently wants to benefit from the CUBA even further. The ERP-software should support different requirements regarding security constrains. In this blog post we will implement the requirements and compare the compile time and runtime approaches to this problem.
Picking up where we left off
In the last blog post about the whole security subsystem in CUBA I mentioned that I might write another blog post to describe additional examples of CUBA security relates features. Although it took quite some time, it finally arrived.
This time, it is mostly about interacting with the security system in a (semi-)programmatic way. Additionally, we will take a slight look at the auditing options of the platform.
If you are not familiar with the basic building blocks of the security features of CUBA, I would encourange you to read about it in the first part.
Extended business-related security constraints
Currently the security constraints that are applied in the software are mostly done through the CUBA role system as well as the database constraints that get applied to the different access groups.
Besides the existing ones, there are a number of additional requirements. We will go through the list one by one and try to implement them. Mostly this is possible not through coding but a semi-programmatic way where we use the feature of the access group constraints and combine it with custom groovy scripts.
But before doing that, let’s try to implement the first requirement in a programmatic way. This way we can look at the benefits and drawbacks a compile time solution has.
You can find all the examples in the sample application: cuba-example-security-constraints on github.
Security at compile time
To get a slow start into this topic, let’s take a look at what solutions are possible in case we don’t know anything about the platform security features. We will use the first requirement (that we will see below in more detail) as a basis for that:
All users can only edit “not closed” orders
Since the only possible choice we have is to develop a piece of software that cares about this check, we will do it programmatically at compile time. One solution is to do a check in the preCommit
action of the Order Editor controller. Here is an example of that:
Here are a few of the benefits that come with this solution:
- code is at the place you expect it to be - an error message on the screen is defined in the corresponding screen controller
- embedded in the source code of the software - runtime configurations can’t accidentily disable security constaints
- fairly easy to test in isolation
Unfortunately there are some major drawbacks as well:
- security logic in UI screens will not prevent other system parts to violate the rule (e.g. the REST API)
- no separation of concerns - UI button logic will seat right next to security constraint logic
- embedded in the source code of the software - not changeable on a per-installation basis
We will care about all those drawbacks in this blog post, but let’s start with the first one. Although the UI might be a big player in terms of data entry, in most cases it is not the only one.
Sometimes you have a REST API (like the CUBA built-in REST API) where you can add data to the system. Or you do a batch import of some kind through another screen. Or think of the BulkEditor in CUBA - all of those possibilities are not aware of your logic in the controller. Therefore the security constraints will not be applied.
For some of those reasons you can copy over the logic (e.g. other screens) to replicate the behavior, but other you just can’t (e.g. BulkEditor) because they are part of the platform and there are no extension points.
Basically this is a show stopper for most of the applications. So let’s think about another solution that is a little bit more low-level: Bean Validation.
Bean Validation as a global way of enforcing security / business rules
In the 6.4 release CUBA added support for Bean Validation. When you look at its website, it says: “Constrain once, validate everywhere” - that’s what we want, right? Fine.
It basically is a Java standard that gives you the possibility to define validation rules on Java classes. CUBA executes those validation rules in the UI and in other parts of the system (e.g. the REST API). With this we have a much better position to do those kinds of checks globally.
So how can a solution to this look like? First we have to create a custom constraint and apply it in the order entity. Detailed information on that can be found in the docs.
In this example, we will create an annotation called CheckOrderNotClosed
with a corresponding validation class called OrderNotClosedValidator
.
The Order entity is annotated with the new Annotation like this:
The actual Validator with the security logic looks like this:
Compared with the first example, this variant of checking security constraints is much better. It is a separation between the screen logic and the the security checking. Additionally it will be used throughout the whole application and has to be defined only once and not for every UI screen.
Nevertheless, it is defined in the source code. This might either be a good thing or a bad thing, depending on your needs.
Security at runtime
In order to give you another tool into your hands, let’s take a look at the other general approach for managing security. This is “implementing security at runtime” through the CUBA built-in features. Mainly it is the feature called access group constraints we already discovered as part of the first blog post.
As described earlier here are some examples that we can go through and try to implement it mainly through runtime configuration of the constraints.
1. All users can only edit “not closed” orders
The first requirement deals with the fact that orders can be updated only when the order is not already in status “closed”.
I will show the solution as the two parts Constraint
and Groovy script
that have to be created in the corresponding access group. After that I will describe the solution and potential problems.
For this requirement there are three possible solutions:
1.1. Solution: simple constraint check
Constraint
order-browse.xml
The already closed orders are no longer editable.
But the are some problems with this solution:
An order that is currently not closed can’t be changed to “orderStatus: closed”, because when trying to change the instance to closed, the constraint gets applied, because the constraint only checks what is currently in memory. It would need some kind of that: old({E}).status != OrderStatus.CLOSED
(see 3. Solution for this)
1.2. Solution: solving the issue through another flag
- create another attribute: “closed: boolean” to the order class.
- create a OrderEntityListener that looks like this:
Constraint
Here, we switched the check to the closed boolean flag. There the above problem does not occur.
1.3. Solution: reloading the entity through dataManager
In this solution we will reload the entity from database, to get the current persistent entity. Then we will compare the persistent entity in order to solve the above mentioned problem.
Constraint
Groovy Script
In this case, we reloaded the entity in order to be able to check more conditions: not only the current state of the entity is important, but also the values that were in the database before.
2. “walter” can only edit his own orders in Northeast
In this example, we want the user walter to constraint in such a way, that the set of orders he is allowed to edit will be reduced futher. Currently he can see (and edit) all orders that are placed by customers in the Northeast area.
Now we want that, although he can see all of those orders, editing is only possible for a subset of those. In this case, he should be be allowed to edit only thd orders created by himself.
Constraint
In the groovy script, certain variables get injected. One of those is userSession
which allows getting information about the current user. In this case we need the login name to compare it to the currents entites createdBy attribute.
3. “walter” can only create non-invoice based orders for new customers
The next example is a little bit more complex. As the company is not willing to trust walter in the same way it trusts other employees, it is required to treat him in a special way. In this case, the following constraint should be applied:
When walter creates an order with a customer that has the customer type “NEW”, it should be only allowed to create this order with particular payment types. New Customers are not allowed to place an order that will be payed via invoice.
Constraint
Groovy Script
To make this work, the view of the order in the order editor has to match with the things that are used in the groovy script (e.g. {E}.customer.type
). Here we see, that there is an implicit correlation between the editor screen definition and the constraint through the need for a particular view. This is somewhat problematic. Because of this, we need to program a little bit more defensive and check in our script.
To check that the attributes are actually in the view (through PersistenceHelper.checkLoaded
) of the entity, it is required to use the magic variable __originalEntity__
that gets passed into the script via SecurityConstraintExtensionImpl
.
{E}
simply does not work (see the link for more information in the comments).We will check that the attributes are loaded via the view in order to not produce false positive results in the script. PersistenceHelper.checkLoaded
will throw an exception if the attribute is not loaded, therefore the script will evaluate to false.
In case the attributes are loaded we will check the attributes for the above mentioned conditions.
4. “saul” can create orders for new customers independent of the payment method
This is a really easy one. As the security constraint only gets applied to the Northeast access group, saul will not be affected by this restriction. Therefore he is capable of creating orders independently of the customers customer type.
5. All users can only “deliver” orders (via a button in the editor of the order), if there is at least one lineItem
In order to achieve this requirement, we need to do a little bit of glue code in the editor of the order. First we will add an appropriate Button and action to the order-edit.xml screen definition like this:
Lets have a look at the deliverOrder
method. It will use the Security platform interface, that allows certain requests against the security subsystem of CUBA in a programmatic fashion.
This solution uses a specific feature of the Constraints. You can define a custom code for a constraint that you will check in the code and define in the access group constraints at runtime.
In this case we use the the variant to check if an entity has a particular permission on a custom constraint code. On github you’ll find the full fletched interface of it, but for this case, we will use the custom constraint code like this:
After that little bit of glue code, we need to define the actual constraint like before. But this time, we will pick the check type “Custom”. This option is relevant for constraint operations that are not directly related to the CRUD operations. In this case we will call it “deliverOrder”.
Constraint
Groovy Script
The groovy script itself just checks if there are line items in place. If so, the operation is allowed.
One could argue that this requirement is probably somewhat unrelated and probably this is true, but as this example should just show the dfferent techniques that can be used it doesn’t really matter for now.
6. All users can only “send invoice” (via a button in the order browser), if the orders payment method is “invoice” and the orderState is “payed”
This last example is fairly similar to the one above. Like before we need to create a little bit of code in our application to make this work. In this case we will go another route in the implementation. The screen definition basically creates button in the buttons panel that uses the sendInvoiceAction
of the table.
In the controller, I created a SendInvoiceAction
class that gets added to the orders table. It is a subclass of ItemTrackingAction
, which enables the feature of setting constraint code. The ItemTrackingAction will check the security constraint and enable / disable the action (and with the the button) accordingly.
In the class definition of the SendInvoiceAction I set the constraint code and some other stuff like icon and caption.
The actionPerform
method is the actual handler of the action. It will delegate the business logic to the InvoiceService.
Summary
CUBA offers the possibility to do most of the security constraits at runtime or at least with part runtime / part coding. This is a higly powerful feature that is actually not very prominent in the docs and described use cases.
The main benefits for this are, that first most of the logic is located in one place. Then you are able to change these settings on a per user / per installation basis, but even if you don’t use that kind of flexibility, you as the developer can decide to structure your implementation in this way to get some of the benefits like separation of concerns etc.
However there are some drawbacks that we should be aware of. It is harder to unit-test compared to the Validator approach (not impossible, but harder). Theoretically it is also possible to change this settings even if you as the developer don’t want that to happen. There are ways around that, but they are more fragile then burning this into the source code.
With this we were able to resolve the requirements regarding additional security constraints on the CuBa Security Inc. I hope I’ve managed to give another tool into your toolbox for implementing security in your CUBA app.