Jump to: navigation, search

Scout/HowTo/3.8/Create Permissions

< Scout‎ | HowTo‎ | 3.8


Scout
Wiki Home
Website
DownloadGit
Community
ForumsBlogTwitter
Bugzilla
Bugzilla


Introduction

Permissions are needed to control:

  1. Who is allowed to do what action
  2. Who is allowed to see which records

Permission to do certain Actions

To enforce that not everybody can do everything, permission are created. A permission has a name, e.g. ReadCompanyPermission. The permissions are grouped into roles, e.g. Standard role or Administrator role. Each user is assigned to one or more roles.

Create a Permission

To create a permission in Scout, use the menu "New Permission..." on folder "Permissions" below folder shared. This creates a class that extends BasicPermission or BasicHierarchyPermission.
When creating e.g. a new Scout Form or Process Service using the Scout SDK, corresponding permissions are created by default as well.

Use Permissions (Client Side)

On a table page, the following methods checks whether the logged in user has the necessary permission and depending on it sets the visibility of the table page to true or false.

@Override
public void execInitPage() throws ProcessingException{
  setVisibleGranted(ACCESS.getLevel(new ReadCompanyPermission()) > BasicHierarchyPermission.LEVEL_NONE);
}

Use Permissions (Server Side)

When creating a process service with Scout SDK, permission classes are created with it. For the methods prepareCreate, create, load and store, the following code is added (on the example of a CompanyProcessService):

public CompanyFormData prepareCreate(CompanyFormData  formData) throws ProcessingException{
  if(!ACCESS.check(new CreateCompanyPermission())){
    throw new VetoException(TEXTS.get("AuthorizationFailed"));
  }
  // TODO business logic here
  return formData;
}
 
public CompanyFormData create(CompanyFormData  formData) throws ProcessingException{
  if(!ACCESS.check(new CreateCompanyPermission())){
    throw new VetoException(TEXTS.get("AuthorizationFailed"));
  }
  // TODO business logic here
  return formData;
}
 
public CompanyFormData load(CompanyFormData  formData) throws ProcessingException{
  if(!ACCESS.check(new ReadCompanyPermission())){
    throw new VetoException(TEXTS.get("AuthorizationFailed"));
  }
  // TODO business logic here
  return formData;
}
 
public CompanyFormData store(CompanyFormData  formData) throws ProcessingException{
  if(!ACCESS.check(new UpdateCompanyPermission())){
    throw new VetoException(TEXTS.get("AuthorizationFailed"));
  }
  // TODO business logic here
  return formData;
}

The ACCESS.check(Permission p) checks, whether the logged in user has the permission p. If not, an exception is thrown, which results in a message box popping up saying that the user is not authorized to do this action.

Assigning Permissions to Users

The permissons are assigned to a user in the AccessControlService [1]. This service is typically created and registered by the Scout SDK when creating a new Scout Project.

Example: Load permissions from a database

Let us say you have a role based security concept with the 2 database tables USER_ROLE AND ROLE_PERMISSION. In the table USER_ROLE, there you link the user to several roles whereas the table ROLE_PERMISSION holds all the permissions that belong to those roles. Then you can select the available permission levels for the current user and then convert it to a permission list using AccessControlUtility.createPermissions(permissionData).

Furthermore you have to add the builtin permission RemoteServiceAccessPermission to the collection of permissions to allow the user to call backend services. The two arguments allow you to constrain the kind of services this user is allowed to call. You can use the wilcard character * to specify a matching pattern.

Your AccessControlService could look like this:

@Override
protected Permissions execLoadPermissions() {
  try {
    Object[][] permissionData = SQL.select(
            "SELECT   P.PERMISSION_NAME, " +
            "         MAX(P.PERMISSION_LEVEL) " +
            "FROM     USER_ROLE R, ROLE_PERMISSION P " +
            "WHERE    R.ROLE_UID=P.ROLE_UID " +
            "AND      P.PERMISSION_LEVEL>0 " +
            "AND      R.USER_NR = :userNr " +
            "GROUP BY P.PERMISSION_NAME"
    );
 
    Permissions p = AccessControlUtility.createPermissions(permissionData);
    p.add(new RemoteServiceAccessPermission("*.shared.*", "*"));
    return p;
  }
  catch (ProcessingException t) {
    //TODO: add logging
    return null;
  }
}

If your permissions are stored elsewhere, no problem. Just prepare the permissionData matrix as required: First column with the name of the class, second column with a numerical "level".

Note.png
Full Access
There exists the permission AllPermission which implies all other permissions. For full-access, please add this permission to the collection.


Fill database tables ROLE_PERMISSION AND USER_ROLE

Put an entry into the table ROLE_PERMISSION to represent the permission. Also, create a role entry to associate this permission with a role the user belongs to. Please note, that in the example above, the PERMISSION_NAME would be x.y.shared.security.ReadCompanyPermission. The level is not of interest for this kind of permission, so simply put PERMISSION_LEVEL=100. More on the topic 'level' will follow further down.

How Permissions are Discovered

Permission are discovered by IPermissionService. The default implementation looks for permission classes in all bundles installed in the OSGi environemnt. Thereby, the following criteria must be satisfied:

  • The class must be of the type java.security.Permission
  • The type must be a public concrete class, meaning not an interface nor an abstract class
  • Class must have the token Permission in its name
  • The class must be located in a package with .security. in its package name
Note.png
Change Permission Discovery
This behavior can be overwritten by writing an own implementation of IPermissionService and giving the own implementation a higher priority.


Fine-grained Access Control

Further, it is possible to use fine-grained access permissions. This is in contrast to the BasicPermission mentioned above, which simply handles 'go' or 'no-go' situations.

However, fine-grained access permissions must be of the type BasicHierarchyPermission. Furthermore, PERMISSION_LEVEL in the database table ROLE_PERMISSION comes into play, meaning that in there, you specify the access level on its behalf the user can access protected resources.

The concept is based on various levels in the range from 0 up to 100. Thereby, 0 means no-access, whereas 100 mean full-access. Basically, if the permission of the user (loaded from database) has a level higher or equals than/to the level requested, access is granted.

In BasicHierarchyPermission, the following levels are defined:

 LEVEL_NONE = 0
 LEVEL_ALL = 100
 LEVEL_UNDEFINED = -1

Again, let us elaborate a tiny example: The requirement would be that users should only access companies which they really belong to. For that purpose, we introduce a new access level LEVEL_OWN=10.

The permission ReadCompanyPermission would be changed as follows:

public class ReadCompanyPermission extends BasicHierarchyPermission {
  public static final int LEVEL_OWN = 10;
 
  private Long m_companyId;
 
  public ReadCompanyPermission(Long companyId) {
    super("ReadCompany" + "." + companyId, LEVEL_UNDEFINED);
    m_companyId = companyId;
  }
 
  protected boolean execCheckLevel(int userLevel) throws ProcessingException {
    if (userLevel == LEVEL_OWN) {
      return SERVICES.getService(ICompanyService.class).isOwnCompany(getCompanyId());
    }
    return false;
  }
 
  public Long getCompanyId() {
  	return m_companyId;
  }
}

In order to enable fine-grained access control so the user can only access her/his personal company objects, the user's permission level must be changed to 10 in the table ROLE_PERMISSION.PERMISSION_LEVEL for the permission ReadCompanyPermission.

The access-check to protect the company resource would be changed as follows:

if (!ACCESS.check(new ReadCompanyPermission(xy))) {
   throw new VetoException("Authorization failed");
}
// user is authorized, do some business logic here

As you may have noticed, there is provided an implicit level in the ReadCompanyPermission's constructor when doing the super call. This level stands for the minimal required level the user must have in order to access the resource. If you would put LEVEL_ALL in there, the user would not be allowed to access the company resource anymore as her/his level is only 10 (LEVEL_OWN) which is lower than 100 (LEVEL_ALL). In difference, the level LEVEL_UNDEFINED (-1) does not represent a concrete level, but exclusively stands for fine-grained access control. As a consequence, the access controller does not decide by itself whether to grant access or not. Thereto, it delegates this decision to you by invoking execCheckLevel(int userLevel) on the permission.

In this example, the provided userLevel would be 10 (LEVEL_OWN). That indicates your code that the caller is only allowed to see companies which he belongs to. In consequence, you have to verify the user's relation to the given company and grant access accordingly.

So feel free to define some other fine-grained access levels, e.g. LEVEL_DEPARTMENT = 20;

Please note, that by calling ACCESS.getLevel(Permission p) the user specific access level on that permission can be requested.

Permission to see certain Records (Row-Level Granting)

To control who is allowed to see certain records, this must be implemented directly in the SQL statement that selects the data from the database into the application. Records for which the logged in user has no right to see, are not returned by the SQL statement. This is only possible if the users, the roles and the permissions are stored in the database.