Access control
Authentication is the process of verifying a user’s identity and authorization is the process of determining whether an authenticated user has access to a particular resource or service. Authorization is critical for ensuring that only authorized users have access to sensitive data and resources. The following chapters describe JUDO’s authentication and authorization process.
An actor represents a generic user of the system and can be used to represent humans or another system. Actors are a special subset of transfer objects. Members of actors can be relations (referred to as access), and event handlers, just like in the case of transfer objects. However, one important difference is that actors do not contain fields and actions.
To define an actor, use the actor keyword.
Syntax:
actor <name> [(<entity> <mapping-field>)] [realm <realm>] [claim <claim>] [identity <identity>] [guard <guard>][purple] { [member] ... }
where the <name> is the name of the actor. The <entity> is the entity type of the linked entity instance. The <mapping-field> is the name of the field that refers to the linked entity instance. The linked entity instance is the persistent representation of the user in the application. If there is no linked entity, the user is not represented in the application. A typical case of not represented user is an anonymous user, who shall not be identified within the application.
The <realm> is the name of the security policy domain attached to the actor. The realm is a collection of users who can be authenticated to use the application. The realms and identity management used by JUDO are out of scope of the application. The default realm management application that can be used by a JUDO application is Keycloack, but any OpenID3 compatible identity management application can be easily integrated.
A claim is a name/value pair that contains information about the user. A collection of claims are sent by the realm management application to our application at the time when the user authenticates. The <claim> selects one of the claims, which will be used for user identification in the application.
The <identity> is an expression that selects an identifier field of the mapped <entity>. The type of the field pointed to by <identity> must be string. The <identity> is used to unambiguously identify the user within the application (e.g. find only one instance of the mapped entity) to define the relationship with the user in the <realm>.
The <guard> is a boolean expression that must evaluate to true in order to authenticate the user. To put it more simply, a guard defines a precondition. If the <guard> evaluates to false, the user is not authenticated and an error message is thrown.
|
There is an alternative syntax to define an actor. This alternative is a bit more expressive, although it may be too verbose. actor <name> [maps <entity> as <mapping-field>] [realm "<realm>"] [claim "<claim>"] [identity <identity>] [guard <guard>] { [member] ... } |
Example:
actor SalesPersonActor(SalesPerson salesPerson)
realm "COMPANY"
claim "email"
identity salesPerson.email;
The above example defines an actor named SalesPersonActor. The company’s sales staff are managed in the "COMPANY" realm. The salespersons' unique identifier is their email address. During authentication, the system selects the instance of the SalesPerson entities whose email field has the same value as the value of the "email" claim.
Access
Authorization is the mechanism responsible for granting external users access to specific transfer objects and their actions. In general terms, authorization is a key mechanism for controlling access rights and permissions.
The actors within the application can be considered the embodiment of external users who can access the transfer objects granted to them. That is, an actor exposes the transfer objects to the outside world.
Access definitions within actors are used to expose the application data model quickly and elegantly. The access can have an expression that will be evaluated when the access is invoked from the outside world. Accesses play the same role as relations in transfer objects.
Use the access keyword to specify an access within an actor.
Syntax:
access <transfer>[[]] <name> [<= <expression>];
The access of an actor is the same as the relation of transfer objects. It may be
Example:
actor CustomerActor(Customer customer) {
access CartTransfer myCart <= customer;
access ProductTransfer[] products <= Product.all();
}
In addition to actors, transfer objects can also provide access to other transfer objects. For instance, if a transfer object represents a customer, it allows access to the customer’s current orders and order history.
The security mechanism in JUDO can be understood as a logical permission graph that defines how external users can access resources in the application. In this graph, the nodes represent actors and transfer objects, while the edges represent the relationships and actions.
At the starting point of this permission graph is the actor, which represents the external user. When a user gains access to transfer objects offered by an actor, they obtain instances of these transfer objects. Once the user has a new transfer object instance, they can then access transfer objects made available by that specific transfer object (and so on). The initial entry point to the permission graph is the actor representing the external user, enabling traversal through relations to access transfer object instances. Consequently, a hierarchical chain of permissions is established as the user can navigate to various transfer object instances. As a result, transfer objects that cannot be reached from the actor remain unavailable to external users.
The comprehensive authorization mechanism can be established by carefully configuring relationships with transfer objects. It’s essential to note that if an external user has already obtained a transfer object instance, they are thereby authorized to access transfer object instances connected through the relationships of that initially obtained transfer object.
JUDO’s security mechanism also includes a number of built-in security features, such as encryption and secure communication protocols, to ensure the privacy and security of the data being exchanged between external users and the application.
Guard
Guards can be defined for actors. A guard is a boolean expression that must evaluate to true in order to continue the execution. To put it more simply, a guard defines a precondition.
To add a guard to actors use guard as follows. The actor guard is evaluated before each request to the application. If the actor guard evaluates to false, none of the transfer objects are available and an error message is thrown.
Example:
actor SalesPersonActor(SalesPerson salesPerson)
realm "COMPANY"
identity salesPerson.email
guard salesPerson.leads.size() > 0;
The example above defines a guard for actor, and the salesperson can only invoke the functions if she has at least one lead.
Permission modifiers
Up until now, we’ve discussed access to transfer objects, which essentially means the permission to read those objects. However, it’s important to note that permission control can extend beyond just reading. We have the flexibility to enhance the rights of access by incorporating additional permissions in the form of modifiers:
-
Create: This permission allows the creation of new objects. Users with create permission can generate new transfer object instances within the system. For example, a user with create permission might be able to create new customer accounts or add new products to a catalog.
-
Delete: The delete permission permits the removal or deletion of objects. Users with delete permission can eliminate existing transfer object instances from the system. For instance, this might involve deleting customer accounts or removing products from a catalog.
-
Update: Update permission grants users the ability to modify or edit existing transfer objects. Users with this permission can make changes to the fields of transfer objects. For example, a user with update permission could edit a customer’s contact information or change the status of an order.
By incorporating these create, delete, and update modifiers into the actor accesses and the relationships, we can provide more extensive and more fine-tuned rights to users. This means that in addition to reading, users can also create, modify, or delete data, depending on the permissions granted to them.
The following example demonstrates how an admin user within the system can be granted specific permissions to create, delete, or update customer instances:
Example:
actor AdminActor(Admin admin) {
access CustomerTransfer[] users <= Customer.all()
create:true
delete:true
update:true;
}
In this code snippet, we define the AdminActor as an actor representing an admin user. This actor is given access to CustomerTransfer objects. Within this context, the create, update, and delete modifiers are utilized. These modifiers are boolean values, which means they can be set to either true or false. If their value is set to true, it signifies that the permission is granted to the admin user, allowing them to create, update, and delete customer instances. Conversely, if the value is false or the modifier is missing the respective permission is not granted.
In the same manner, permissions can also be incorporated into relationships between transfer objects:
Example:
transfer CustomerTransfer(Customer customer) {
relation OrderTransfer[] orders <= customer.orders
create:true
delete:true
update:true;
}
This second example showcases how permissions can be extended to the relationships of transfer objects. Here, we define a CustomerTransfer object with a relationship to OrderTransfer objects. Once again, we employ the create, update, and delete modifiers. These modifiers dictate whether the user who has obtained a CustomerTransfer object is permitted to create, delete, or update the customer’s associated orders. If the modifiers are set to true, the user can perform these actions, otherwise not.
|
It is important to note that it is not enough to set the |
This approach provides precise control over what actions users, particularly admins, can undertake concerning specific objects and their associated objects. It enhances the system’s security and flexibility by allowing the fine-tuning of access permissions.
Input modifier
Permission modifiers provide users with the ability to modify, delete, or create objects. However, when a user is granted permission to modify an object, the question arises as to which fields and relationships of the given object can be altered.
By default, every field of a transfer object is set to read-only. To allow changes to a specific field of a transfer object, the input modifier should be utilized. Fields that lack the input modifier set to true are off-limits for user modifications.
Example:
transfer PersonTransfer(Person person) {
field String firstName <= person.firstName input:true;
field String lastName <= person.lastName input:true;
field String email <= person.email;
}
In the provided example, a user with update permission can modify the first and last name of the person if necessary. However, the email address remains a read-only field, even if the update permission is granted. This restriction also holds true for the create permission.
This approach allows for fine-grained control over which fields of an object can be modified, ensuring data integrity and security while permitting necessary changes when required.
Choices modifier
By default, relations between transfer objects are configured as read-only, similar to fields. However, when you need to allow modifications to these relations, you have two options. One way to modify relations is by using the create and delete permissions, while the other is by adding or removing transfer object instances employing the choices modifier.
The choices modifier enables you to specify which instances can be added to or removed from the relation. Instances not listed in the choices won’t be allowed to be added. If you want to allow the addition of any instances, you can use choices as shown in this example:
Example:
transfer ProductTransfer(Product product) {
relation ProductTransfer[] recommendations <= product.recommendations
choices:Product.all();
}
In this example, the choices modifier is set to Product.all(), which means any product instance can be added to the recommendations relation.