Design time roles - wildcards and packages

Hi,
I switched recently to security roles v2 and checking the documentation. The documentation on @EntityAccess and @EntityAttributeAccess show you can either use the entityClass attribute or the entityName attribute.

To be honest, neither is ideal for me. I had a look and have about 185 entities today and counting.The risk of me adding entities and forgetting to update the roles is more than realistic. Meaning running into runtime issues. So manually adding all the entityclasses would not only be unreadable it’s unmanageable.

Regarding the entityname atrribute, it is not documented what is valid in terms of wildcards. The examples either give full access to everything * or just a single entity. So no idea if “mydomain$*” would be allowed.

Now, in order to have my domain model somewhat structured, I used a construct called “packages”. This way aggregate root entities and related entities are nicely put together in subdomains. Wouldn’t this be a more developer-friendly way of solving this?

update: I tried using wildcards in the entityname, it seems mydomain* is not supported
update 2: see for a solution below

Ok, i have been digging through the underlying code and it seems a solution would not be too complicated.
If there would be a package attribute added to the annotations and the com.haulmont.cuba.security.app.role.AnnotatedPermissionsBuilder#processEntityAccessAnnotation (and similar) method adapted it should work.

Another solution would be to have some kind of fluent interface for building up the roles as an alternative to the annotations.

Following code is very crude btw, consider it a poc. But it does give me the wanted behavior.

package com.mydomain.roles;

import com.google.common.base.Strings;
import com.haulmont.cuba.core.global.Metadata;
import com.haulmont.cuba.security.app.role.AnnotatedRoleDefinition;
import com.haulmont.cuba.security.app.role.annotation.EntityAccess;
import com.haulmont.cuba.security.app.role.annotation.EntityAttributeAccess;
import com.haulmont.cuba.security.app.role.annotation.Role;
import com.haulmont.cuba.security.app.role.annotation.ScreenAccess;
import com.haulmont.cuba.security.entity.Access;
import com.haulmont.cuba.security.entity.EntityOp;
import com.haulmont.cuba.security.role.EntityAttributePermissionsContainer;
import com.haulmont.cuba.security.role.EntityPermissionsContainer;
import com.haulmont.cuba.security.role.PermissionsUtils;
import com.haulmont.cuba.security.role.ScreenPermissionsContainer;

import javax.inject.Inject;

@Role(name = "Business Entities Crud")
public class BusinessEntitiesCrudRole extends AnnotatedRoleDefinition {
    @Inject
    private Metadata metadata;

    @Override
    @EntityAccess(operations = {EntityOp.CREATE, EntityOp.READ, EntityOp.UPDATE, EntityOp.DELETE})
    public EntityPermissionsContainer entityPermissions() {
        EntityPermissionsContainer entityPermissionsContainer = new EntityPermissionsContainer();
        metadata.getClasses().stream()
                .filter(m -> m.getJavaClass().getPackage().getName().startsWith("com.mydomain.entity"))
                .forEach(m -> addToPermissionmContainer(m.getName(), entityPermissionsContainer, EntityOp.values()));
        return entityPermissionsContainer;
    }

   //stolen and stripped from the AnnotatedPermissionsBuilder
    private void addToPermissionmContainer(String entityName, EntityPermissionsContainer permissions, EntityOp... operations) {
        for (EntityOp entityOp : operations) {
            String target = PermissionsUtils.getEntityOperationTarget(entityName, entityOp);
            Integer permissionValue = Access.ALLOW.getId();
            permissions.getExplicitPermissions().put(target, permissionValue);
            String extendedTarget = PermissionsUtils.evaluateExtendedEntityTarget(target);
            if (!Strings.isNullOrEmpty(extendedTarget)) {
                permissions.getExplicitPermissions().put(extendedTarget, permissionValue);
            }
        }
    }

Going in futher, I just noticed that there is no information about screens available when defining roles.

The documentation states that role definitions should be declared in the core module. Hence I have no access to any screen metadata.

Similar to the entities I have hundreds of screens, Having to manually add/update them whenever a screen is added or changed is just not feasible.

Hi,

Excuse me, but this is ridiculous!
This is security setup. It’s one of the most important aspects of your system.
It’s your responsibility of developer to allow users access only that amount of functionality that they need, and nothing more.
Your clients trust you that you’re developing secure software. It is your legal obligation to develop secure software (General Data Protection Regulation).

There is no other way to maintain system secure other than:

  1. Carefully think about each system feature and each role and determine which feature should be allowed to each role.

  2. Perform security testing as one part of QA process. Test that security isn’t broken after releasing new version (this is regarding “risk of me adding entities and forgetting to update the roles is more than realistic”). To reduce risk of security regression one should invest into automation of UI test scenarios (use GitHub - cuba-platform/masquerade: CUBA Platform UI Testing Library for that).

No one said that setting up software security will be easy or fun. It’s not, that’s why it is often neglected by software developers and under-funded by management.

1 Like

Yes, you can write your own implementation of entityPermissions() and other methods, if you can specify entity permissions in shorter way than enumerating all entities one by one. This possibility was kept in mind by CUBA team when design-time roles were implemented.

So which one is it? Is it ridiculous or is it a possibility that was kept in mind by CUBA team when design-time roles were implemented.

The possibility to write your own implementation of entityPermissions() and other methods was intentionally provided by the CUBA team.

There is nothing ridiculous about having a domain model with aggregate roots and related objects. And there is also nothing ridiculous that business roles have the same permissions on different objects in such an aggregate graph. It is exactly because of carefully modelling domains and subdomains based on business roles that permissions arise. And these permission do not have to be limited to entities but could in fact be defined over domains.

In a Cuba app the whole browser-editor screens concept clearly steers us into having a dedicated editor screen on a per entity basis. It is not ridiculous for a user to have access to a screen if a user has crud rights on the underlying entity.

Yes, you can write your own implementation of entityPermissions() and other methods, if you can specify entity permissions in shorter way than enumerating all entities one by one. This possibility was kept in mind by CUBA team when design-time roles were implemented.

Well than you did a pretty good job of hiding it since I had to dive into the internal workings of the security to see how to get it done.

Many applications use simple RBAC security on the service layer without doing checks on the underlying model. These are choices that have been made but do not make all of these are projects ridiculous because they dont have detailed entity security.

I like the cuba setup of the entity and attribute based security. What I don’t like is the cumbersome way of declaring the roles.

The only thing that is ridiculous here is your response. Your reference to GDPR and “legal obligation” is completely irrelevant and shows you have no clue what GDPR even means. In fact the whole response is irrelevant.

It is not about making my security setup fun, it is about being able to express my security setup in a way that it is clear and manageable. There is no way that you could argument that manually adding 100 entities is a better than using some kind of expression (whatever that expression might be). Your arguments about QA and security testing is also completely irrelevant. I have not at any point spoke for or against security testing it is completely orthogonal against how the security roles are expressed.

So basically, removing all the irrelevant blabber from your response only leaves one sentence left: “It is ridiculous”

Fantastic!
 @EntityAccess(entityClass = Bird.class, Horse.class, Dog.class. Pinguin.class, Elephan.class, ....,
            operations = {EntityOp.CREATE, EntityOp.READ, EntityOp.UPDATE, EntityOp.DELETE})


Ridiculous!
 @EntityAccess(package = com.myzoo.animals, ....,
            operations = {EntityOp.CREATE, EntityOp.READ, EntityOp.UPDATE, EntityOp.DELETE})

Whoa whoa whoa guys, I have no idea how this went off the rails into such hostility. I’m gonna guess a language barrier issue of some sort. (@stukalov or someone else might want to moderate this!)

As far as Tom’s original point - it does seem that some kind of easier way to handle permissions would be good - especially if you organize your entities the way Tom has.

This rings true. If a user has CRUD permissions for an entity, it follows they need same for the screen for said entity, otherwise they cannot exercise said permissions! It should follow if one has CRUD access to someapp.entity.foo, one should also have access to someapp_Foo.edit and someapp_Foo.browse…

First, please don’t misquote my words. I have not called ridiculous the structure and organization of your code with aggregate roots.
I have quoted two paragraphs where as I feel some thoughts are expressed which can be error-prone and leading to security holes in resulting software (not necessarily in your case, maybe in your particular project it works perfectly, but for general public they may lead to problems).

  1. You mention 185 entities. If it is the first time you introduce design time roles into your project - well, yes, it is going to take some time and effort. Of course it is expected that introducing design-time roles into already existing big project may mean one needs to take some measurable coding efforts.

  2. You mention that “The risk of me adding entities and forgetting to update the roles is more than realistic”. This is true, there is always a risk to forget to add a permission for a screen or entity - that is - to permit opening a screen or reading an entity. But let’s consider two types of risks:

  • User can’t access what he/she needs to access (this is our case with design-time roles).
  • User is allowed to access what he/she must not access, because wildcard package permissions allowed him so, and developer forgot to exclude one specific entity from the package.

Second risk is a security problem, and it’s worse. This is why for general public allowing entities one by one is more safe than adding wildcard permissions by package. CUBA platform and CUBA Studio are intended for wide audience, and therefore CUBA team chooses decisions that lead to higher quality software in general. (Note that shorter code does not mean higher quality software.)

Valid point…
Anyway this is what open source is about. We expect from the person who want to hack the platform, make life easier for himself - to spend some time studying the source code.

I agree, I have actually worked on a project that didn’t have detailed entity security.
But in CUBA we also must keep in mind not just Web UI, but also REST API clients (and frontend React-based clients). For REST-based clients detailed entity security is a must (unfortunately).

Thanks for sharing your feedback, we will take it into account (along with idea for package wildcard syntax) for future improvements in the design-time security subsystem.

The GDPR is so broad and abstract that it concerns even basic security measures for software. Let’s take a look to the text (Chapter 2):

Personal data shall be: …
processed in a manner that ensures appropriate security of the personal data, including protection against unauthorised or unlawful processing and …
Art. 5 GDPR – Principles relating to processing of personal data - General Data Protection Regulation (GDPR)

Or do you mean that software developer isn’t the “controller”, neither the “processor”, and therefore isn’t covered by GDPR requirements?

I’ve explained above: exposing a data (e.g. through REST web service) that shouldn’t be accessible to the user (it’s a security problem) is worse than non-giving access to the screen that should be accessible (it’s just a bug).

Probably I misread, but I suspected from your post that a developer who cares a lot about making a mistake in a role definition - probably doesn’t perform QA for these definitions.

I hope for this time I explained my concerns better.


Again, we really appreciate that you spent your time sharing your impressions about design-time roles with us, the feedback is really valuable for us and we will take it into account in the future CUBA versions.

Hi,
I think that this can be implemented as an optional assistance in the role editing UI. It would simplify setting up role permissions in runtime role editor and in Studio’s role designer.

However there are lots of situation where having access for the “A” entity doesn’t mean that this user should have “A browser/editor” in his/her main menu.

Example from CUBA:
In order to setup new sec$User entities (in User Editor), the user should have READ access for the sec$Role entity, to be able to assign User Roles in this screen.
But that doesn’t mean that the same user should be able to have “Roles” browser in his/her main menu and be able to open this screen (Role lookup and Role browser may be different screens).

The same for Reports. To run a report, you need to be able to read report$Report entity. But this doesn’t mean you need a Report browser (that screen is actually a privileged screen for admins with export/import abilities).

Also, there are users that don’t consume Web UI (web service clients), those shouldn’t be granted with menu/screen permissions.

Fair enough for the examples you gave, but in most business applications if a person has CRUD perms on, say, “Customer,” they should (and nearly always will) have access to the screens to manipulate “Customers.” Otherwise they cannot really make use of those CRUD permissions…

I think the main reason for these examples is because they are Cuba internal entities that actually give technical functionality instead of business functionality.

As @jon.craig states, for most business entities the crud operations will match the UI.

Is this going to be the case in every possible application? No, not at all. But it will be the case in many Cuba based applications. Why? Exactly because Cuba gives us UI scaffolding over our entities.

This is not a just a coincidence. It is the main reason why many of us chose Cuba over just standard spring and wiring in some other libraries. I do some front-end stuff on my other projects but most of the time my design becomes a class example of developer art.

The scaffolding together with the base screen features (filters, saved searches, related entities…) allow me to rapidly create UI (which the end user sees) without too much of a hassle and being able to concentrate on business logic.

2 Likes

One more post and I promise :v: to leave it at that (:crossed_fingers:)

my deliberate usage of the vague term “some kind of expression” should perhaps be replaced by … code … allow us to use the language we know best.

I will post my current setup here, perhaps other people will find it useful.

The first class is an example role.
For entity access I only use the packagename.
For entity attribute access I added a method that also allows to pass in a predicate over fields.
In this completely useless example this role gets modify access on all non-enum simple types. It also gets view access on all fields that have the NotNull annotation.

Pretty useless example, but it does show that with a bit of code you can now express powerful things. You could introduce eg. annotations and couple a role to it. This would allow to remove runtime reference to properties names.

The main point I guess that it gives us developers the tools to integrate our specific behavior into generic Cuba mechanisms.

@AlexBudarov @jon.craig i would like you guys opinion on this.

package mypackage;

import com.haulmont.chile.core.model.MetaProperty;
import com.haulmont.cuba.security.app.role.AnnotatedRoleDefinition;
import com.haulmont.cuba.security.app.role.annotation.Role;
import com.haulmont.cuba.security.entity.EntityAttrAccess;
import com.haulmont.cuba.security.entity.EntityOp;
import com.haulmont.cuba.security.role.EntityAttributePermissionsContainer;
import com.haulmont.cuba.security.role.EntityPermissionsContainer;

import javax.inject.Inject;
import javax.validation.constraints.NotNull;

@Role(name = "Business Entities Crud")
public class BusinessEntitiesCrudRole extends AnnotatedRoleDefinition {
    @Inject
    private EntityPermissionContainerBuilderFactory entityPermissionContainerBuilderFactory;

    @Override
    public EntityPermissionsContainer entityPermissions() {
        return entityPermissionContainerBuilderFactory
                .entityPermissionsContainer()
                .add(m -> m.getJavaClass().getPackage().getName().startsWith("mypackage.entity"), EntityOp.values())
                .build();
    }

    @Override
    public EntityAttributePermissionsContainer entityAttributePermissions() {
        return entityPermissionContainerBuilderFactory.entityAttributePermissionsContainerBuilder()
                .add(m -> m.getJavaClass().getPackage().getName().startsWith("mypackage.entity"),
                        mp -> mp.getType() == MetaProperty.Type.DATATYPE, EntityAttrAccess.MODIFY)
                .add(m -> m.getJavaClass().getPackage().getName().startsWith("mypackage.entity"),
                        mp -> mp.getAnnotatedElement().isAnnotationPresent(NotNull.class), EntityAttrAccess.VIEW)
                .build();
    }
}

package mypackage;

import com.haulmont.cuba.core.global.Metadata;
import org.springframework.stereotype.Component;

import javax.inject.Inject;

@Component
public class EntityPermissionContainerBuilderFactory {
    @Inject
    private Metadata metadata;

    public EntityPermissionsContainerBuilder entityPermissionsContainer() {
        return new EntityPermissionsContainerBuilder(metadata);
    }

    public EntityAttributePermissionsContainerBuilder entityAttributePermissionsContainerBuilder() {
        return new EntityAttributePermissionsContainerBuilder(metadata);
    }
}

package mypackage;

import com.google.common.base.Strings;
import com.haulmont.chile.core.model.MetaClass;
import com.haulmont.cuba.core.global.Metadata;
import com.haulmont.cuba.security.entity.Access;
import com.haulmont.cuba.security.entity.EntityOp;
import com.haulmont.cuba.security.role.EntityPermissionsContainer;
import com.haulmont.cuba.security.role.PermissionsUtils;
import lombok.AllArgsConstructor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class EntityPermissionsContainerBuilder {
    private Metadata metadata;
    private List<EntityPermissionEntry> permissionEntries = new ArrayList<>();

    EntityPermissionsContainerBuilder(Metadata metadata) {
        this.metadata = metadata;
    }

    public EntityPermissionsContainerBuilder add(MetaClass metaClass, EntityOp... permissions) {
        Arrays.stream(permissions).map(p -> new EntityPermissionEntry(metaClass, p)).forEach(permissionEntries::add);
        return this;
    }

    public EntityPermissionsContainerBuilder add(Predicate<MetaClass> predicate, EntityOp...entityOp) {
        metadata.getClasses().stream()
                .filter(predicate)
                .flatMap(m -> Arrays.stream(entityOp).map(op -> new EntityPermissionEntry(m, op)))
                .forEach(permissionEntries::add);
        return this;
    }

    public EntityPermissionsContainer build() {
        EntityPermissionsContainer container = new EntityPermissionsContainer();
        permissionEntries.stream().forEach(p -> addToPermissionToContainer(container, p.metaClass, p.entityOp));
        return container;
    }

    private void addToPermissionToContainer(EntityPermissionsContainer permissions, MetaClass metaclass, EntityOp entityOp) {
            String target = PermissionsUtils.getEntityOperationTarget(metaclass.getName(), entityOp);
            Integer permissionValue = Access.ALLOW.getId();
            permissions.getExplicitPermissions().put(target, permissionValue);
            String extendedTarget = PermissionsUtils.evaluateExtendedEntityTarget(target);
            if (!Strings.isNullOrEmpty(extendedTarget)) {
                permissions.getExplicitPermissions().put(extendedTarget, permissionValue);
            }
    }

    @AllArgsConstructor
    private static class EntityPermissionEntry {
        MetaClass metaClass;
        EntityOp entityOp;
    }
}
package mypackage;

import com.google.common.base.Strings;
import com.haulmont.chile.core.model.MetaClass;
import com.haulmont.chile.core.model.MetaProperty;
import com.haulmont.cuba.core.global.Metadata;
import com.haulmont.cuba.security.entity.EntityAttrAccess;
import com.haulmont.cuba.security.role.EntityAttributePermissionsContainer;
import com.haulmont.cuba.security.role.PermissionsContainer;
import com.haulmont.cuba.security.role.PermissionsUtils;
import lombok.AllArgsConstructor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class EntityAttributePermissionsContainerBuilder {
    private Metadata metadata;
    private List<EntityAttributePermissionEntry> permissions = new ArrayList<>();


    EntityAttributePermissionsContainerBuilder(Metadata metadata) {
        this.metadata = metadata;
    }

    public EntityAttributePermissionsContainerBuilder add(MetaClass metaClass, EntityAttrAccess attrAccess, String... properties) {
        Arrays.stream(properties)
                .map(p -> new EntityAttributePermissionEntry(metaClass, p, attrAccess))
                .forEach(permissions::add);
        return this;
    }

    public EntityAttributePermissionsContainerBuilder add(Predicate<MetaClass> predicate, EntityAttrAccess attrAccess, String ... properties) {
        metadata.getClasses().stream()
                .filter(predicate)
                .flatMap(m -> Arrays.stream(properties).map(p -> new EntityAttributePermissionEntry(m, p, attrAccess)));
        return this;
    }

    public EntityAttributePermissionsContainerBuilder add(Predicate<MetaClass> predicate, Predicate<MetaProperty> fieldPredicate, EntityAttrAccess entityAttrAccess) {
        metadata.getClasses().stream()
                .filter(predicate)
                .flatMap(m -> m.getProperties().stream()
                        .filter(fieldPredicate)
                        .map(p -> new EntityAttributePermissionEntry(m, p.getName(),  entityAttrAccess))
                ).forEach(permissions::add);
        return this;
    }

    public EntityAttributePermissionsContainer build() {
        EntityAttributePermissionsContainer container = new EntityAttributePermissionsContainer();
        permissions.forEach(p -> addEntityAttributeTarget(container, p.metaClass.getName(), p.property, p.access));
        return container;
    }

    private void addEntityAttributeTarget(PermissionsContainer permissions, String entityName, String property,
                                            EntityAttrAccess access) {
        String target = PermissionsUtils.getEntityAttributeTarget(entityName, property);
        Integer permissionValue = access.getId();
        permissions.getExplicitPermissions().put(target, permissionValue);
        String extendedTarget = PermissionsUtils.evaluateExtendedEntityTarget(target);
        if (!Strings.isNullOrEmpty(extendedTarget)) {
            permissions.getExplicitPermissions().put(extendedTarget, permissionValue);
        }
    }

    @AllArgsConstructor
    private static class EntityAttributePermissionEntry {
        MetaClass metaClass;
        String property;
        EntityAttrAccess access;
    }
}

1 Like