Sunday 20 November 2011

Using Liferay's Permission System from a portlet

Liferay's permission system is a very flexible mechanism to define the actions a given user can perform within the general context of the portal, or within a given portlet and its data. Portal and portlet developers break down the operations which can be performed within the portlet, and/or on the data they create, into distinct "actions". The act of granting the ability to perform a given action to an individual user, role, user group, etc. is of course the act of granting that permission. In this way, portal/portlet developers are not tied to named roles, user group, or any other authorization model in particular, all they need to care about is clearly defining the different types of operations required to suite the business logic of their applications.
Once the actions have been identified and configured, the system administrator is free to grant permissions to perform those actions to users, groups, communities, etc, either directly or through roles, using the portal's administration tools or specifically within portlet's configurations UIs.
The purpose of this article is to explain how a portlet developer can use this service during development to give the portal administrator the same level of control over permissions that she has over the bundled Liferay portlets.

Important concepts #

The main concepts behind Liferay's permission system are those of actions and resources. Examples of actions would be: VIEW, UPDATE, MOVE, etc. Examples of resources could be: RSS portlet hF5f, the message board post with id 5, an event of the community X, a wiki page of the portal. Note that these four examples each have a different degree of specificity. That's one of the reasons for the flexibility of the system and we'll get back to it in detail later.
Permissions in Liferay are defined as actions on a given resource. For example view the message board post with id 5, edit events of community X, etc. Permissions can be assigned directly to users, although it's recommended to do it through roles (which can be seen as a group of permissions), user groups, organizations or communities.
In order to use the permission system, a portlet developer must follow these steps:
  1. Define the types of resources of the portlet and the actions that can be performed on each. This is done in an XML file.
  2. If the portlet defines a domain model, create the resource (or resources) which can be controlled through the permission system. This is done through a API call. An example of this are Message Board Threads.
  3. Add permission checks whenever a user is performing a defined action. This is also done through a API call.
One important point related specifically to the portlet resource itself, is that, by default, and regardless of the actions specified by the developers, the portlet will implicitly have two actions defined on it. These are CONFIGURATION and VIEW.
:;CONFIGURATION : involves, at the least, performing maintenance operations that are provided through portal functionality such as adjusting the look-and-feel of the portlet, changing it's title, and assign permissions on the portlet. :;VIEW : involves two facets, the first is whether to allow a user, who has the ability to see and use the Add Content/Application/Portlet menu, to see a listing of the portlet, and secondly, if the portlet is already on a page/layout, whether that user can see the portlet or either a message indicating they don't have permission, or nothing at all.
In some cases it might be desirable to do some further steps to add more functionality. But we'll leave that for a later section.

Learning with a real world example #

Introducing the example: student scores management #

The example that we are going to use is that of a portlet that allows a teacher to record the scores of his students. We'll assume that the teacher and the students each have individual portal accounts already. The system will have two types of persistent entities: Test and Score (of a student in a test).

Step 1) Defining the types of resources #

The definition of the portlet resources is done in an XML file that should be placed in the classpath. Liferay's convention is to place it in the portal-impl/classes/resource-actions/ directory and give the file the 'semantic' name of the portlet (such as messageboards.xml). (In the ext environment it change portal-impl for ext-impl in the previous path).
We'll name the file studentscores.xml and create it with the following content:
<resource-action-mapping>
    <portlet-resource>
        <portlet-name>studentscores</portlet-name>
        <supports>
            <action-key>ADD_TEST</action-key>
            <action-key>CONFIGURATION</action-key>
            <action-key>VIEW</action-key>
        </supports>
        <community-defaults>
            <action-key>VIEW</action-key>
        </community-defaults>
        <guest-defaults>
            <action-key>VIEW</action-key>
        </guest-defaults>
    </portlet-resource>
    <model-resource>
        <model-name>com.liferay.portlet.studentscores.model.SCTest</model-name>
        <portlet-ref>
            <portlet-name>studentscores</portlet-name>
        </portlet-ref>
        <supports>
            <action-key>ADD_SCORE</action-key>
            <action-key>DELETE</action-key>
            <action-key>UPDATE</action-key>
            <action-key>VIEW</action-key>
        </supports>
        <community-defaults />
        <guest-defaults />
    </model-resource>
    <model-resource>
        <model-name>com.liferay.portlet.studentscores.model.SCScore</model-name>
        <portlet-ref>
            <portlet-name>studentscores</portlet-name>
        </portlet-ref>
        <supports>
            <action-key>DELETE</action-key>
            <action-key>UPDATE</action-key>
            <action-key>VIEW</action-key>
        </supports>
        <community-defaults />
        <guest-defaults />
    </model-resource>
  </resource-action-mapping>
Let's review the file from beginning to end. The first thing to note is that it defines 3 resources.
The first one is a resource for the portlet itself. By defining it, the administrator will be able to configure which users can perform each operation on the portlet when it's placed in a page. This resource supports 3 actions: :;ADD_TEST: the user with the permission to perform this action is allowed to add a new test through the portlet. :;CONFIGURATION: the user with this permission can alter the look-and-feel, change the title, and assign permissions on the portlet. :;VIEW: the user with the permission to perform this action is allowed to view the contents of the portlet, and, if they are a layout manager (see definition bellow), see it in the Add Content/Application/Portlet menu.
The second resource represents a test created through the portlet. It supports the following actions: :;ADD_SCORE: Add the score of a student for this test :;DELETE: Delete the test :;UPDATE: Update the test :;VIEW: View the test
The score also has an associated type of resource with the following actions: :;DELETE: Delete the score :;UPDATE: Update the score :;VIEW: View the score
Once this file is finished we have to tell Liferay to actually load the file. This is done by a line in portal-ext.properties such as:
resource.actions.configs=resource-actions/studentscores.xml
Now we are ready to start the development part of our assignment.

Step 2) Create the resources that will be controlled by the permission system #

Whenever a new test or a new score is added through our portlet we need to inform the permission system of it so that it can create a set of resources associated with them. This is achieved through a call to the ResourceLocalService. This service can be invoked through the ResourceLocalServiceUtil class or by receiving an instance through dependency injection. In this example we'll assume the latter. So after our custom code has created a new test we invoke:
resourceLocalService.addResources(
     test.getCompanyId(), test.getGroupId(), test.getUserId(),
     SCTest.class.getName(), test.getTestId(),
     false, addCommunityPermissions, addGuestPermissions);
There are several things to note here:
  1. The test class contains a companyId field. This is necessary to support Liferay's multi-instance functionality
  2. The test class contains a groupId field. This means that our tests will be scoped by group (that is by community or organization). If we had decided to have a unique set of tests for the whole portal instance we should have specified 0.
  3. The third parameter, userId, is the userId of the user that created the resource. The permission system will automatically give all permissions to this user.
  4. The fourth parameter, name identifies the type of resource and by convention it is recommended to use the class name that represents this type of entries.
  5. The fifth parameter, primKey, must be a unique id of the current entry
  6. The sixth parameter, portletActions, must be false because this is not an entity that represents a portlet. You will hardly ever need to set this parameter to true.
  7. The seventh and eight parameter determine if a set of permissions should be given to the owners of the group to which the test belongs and to anonymous users or not. That set of permissions is configured through the XML file. We left both sets empty so it doesn't make any difference to us. You can just give them a value of false.
It's interesting to understand what happens when this method is invoked. In short it will create several resources associated with the test we just created. Let's see why more than one resource and the purpose of each of them:
1) A resource with scope individual which represents one and only one SCTest (the one we just created). This resource will allow the administrator to set up permissions to operate on this specific SCTest. 2) A resource with scope group. This is done only once for the first SCTest created within this group. All others will share the same resource. This resource will allow the administrator to set a permission that applies to all SCTest within a given community or organization. Usually this is done through regular roles. 3) A resource with scope group_template. It's very similar to the previous but it's used slightly different by administrator. This resource allows a community or organization role to include a permission to do an action on all SCTest within a given community or organization. Which community or organization will be determined when the role is assigned to a user. 4) A resource with scope company. This is only done for the first template created within this company. This resource allows an administrator to set a permission that applies to all SCTest in the whole portal instance (called Enterprise in the permissions administration UI).
Whenever permissions are checked for a SCTest entity it checks first if it has permission in the associated individual scoped resource, then in the group scoped resource and then in the company scoped resource.
We are ready to test it. But first lets sure the system is always in good shape and does not contain resources of SCTest that do not exist any more. To achieve that when a SCTest is deleted we should also delete its associated resources. Actually we only have to delete the resource with scope individual since all other are shared with other tests. So we invoke:
resourceLocalService.deleteResource(
     message.getCompanyId(), SCTest.class.getName(),
     ResourceImpl.SCOPE_INDIVIDUAL, test.getTestId());
For each score that we add or delete we should do the same thing that we've just shown for tests.

Step 3) Add permission checks whenever a user is performing a restricted operation #

Our third step will be to check whether a user has permission to do a certain action when he tries to do it. First we'll check whether the user has permission to add a new test.
The API to check permission is centralized in a class called PermissionChecker (for the curious, actually that's an interface, and the actual class provided by default to implement the interface is PermissionCheckerImpl). Liferay creates an instance of that class for every request and reuses it for all checks for permissions to ensure maximum eficiency. To obtain the current permission checker the easiest way is to use Liferay's taglib theme:defineObjects:
<theme:defineObjects/>
After invoking this tag a JSP variable called permissionChecker will be available. If you don't want to use the tag you can access the permission checker using the following code (add the imports necessary to make it run):
<%
  ThemeDisplay themeDisplay = (ThemeDisplay)request.getAttribute(WebKeys.THEME_DISPLAY);
  PermissionChecker permissionChecker = themeDisplay.getPermissionChecker();
 %>
Once we have the PermissionChecker instance we can invoke its method contains to find out if the user has the desired permission:
permissionChecker.hasPermission(
        layout.getGroupId(), "studentscores", testId, "ADD_SCORE");
As a result of this invocation our code should throw an Exception if the user is not allowed to do the requested action and the UI should catch that exception and show a message to the user. Liferay's portlets use PrincipalException for that task, so feel free to reuse it.
Important Note: by passing the groupId to permissionChecker we are implicitly telling the permission system that this action is performed within the context of the current group. Because of that an administrator of the current group will always have permission to perform that action even if it wasn't assigned explicitly. If you don't want that use 0 as the groupId. Note that in that case permissions to perform this action within an specific community will have no effect.
The same check should also be applied in the UI to show or hide the actions that are presented to a user based on his privileges. For example:
<c:if test="<%= permissionChecker.hasPermission(layout.getGroupId(), "studentscores", testId, "ADD_SCORE");
 %>">
    <a href="<%= addScoreURL %>" >Add score<a/>
 </c:if>

Test it #

Once we've added the appropriate checks all around our portlets we are ready to test. The easiest way to do it is by creating custom roles that give different permissions within our portlets.
To do that go to the Enterprise Admin portlet and create one or more regular roles. For each role chosse the "Define permissions" action. Then click the "Add portlet action" and look for your portlet. When you click on it you'll see a form with all the actions that can be performed on each of the resources defined in the XML file above. (Note: for versions of Liferay Portal prior to 4.4 this screen is divided, you'll have to navigate using the links to configure each resource)
Note that for each action you can just leave the combo next to it blank (which means don't assign the permission), set it to enterprise (which means allow it for all resources within the portal) or set it to community (which means allow it for specific communities). For the latter option you'll have to choose one or more communities using the provided selection pop-up.
Once the role or roles have been defined assign it to a user and go to your portlet to test out the different permissions.

Getting more out of the permission system #

Utility classes to check permission #

The PermissionChecker API is pretty low level. Usually portlet developers develop classes around it to ease checking permissions. Liferay Portal comes with several classes available that can be used and others that can be used as an example to build your own. Here are some significant ones:
:;PortletPermissionUtil: useful for checking "portlet" level actions. In our case that would be VIEW, CONFIGURATION and ADD_TEST. Note that this class grants a user with UPDATE permission in the page where the portlet is, with the permission to do any action, by default. In order to prevent this in prior versions just don't use this class and use permissionChecker directly. Beginning with 4.4, this can be disabled by using the 'strict' parameter set to true; although, this does not prevent higher level layout managers (Community Administrators/Owners, like Power Users) from performing any action on the portlet. In order to do that you must explicitly indicate what actions these users can perform by adding a layout-manager element to the portlet resource configuration: e.g. This limits the layout manager to only VIEW action by default.
<layout-manager>
        <action-key>VIEW</action-key>
    </layout-manager>
:: Of course further permissions can be granted to these users through explicit assignment. But remember, that by leaving this out, the layout manager will be able to perform ALL actions available on the portlet. :;PortalPermissionUtil: useful for checking "portal" level actions. This is useful when we have some actions that do not really belong to a given portlet nor want to be assigned for a specific community. An example would be a permission to 'Add Users' to the portal. :;BookmarksFolderPermission: this is a class created explicitly to check permissions to operate with folders of the Bookmarks portlet. You probably won't be using it (unless you are interacting with that portlet). But you may want to take a look at its code to create a similar one for your own data. In our example we could create a SCTestPermission class and a SCScorePermission class.

Default permissions #

Often we want our data entries to have certain default permissions when they are created. That can be achieved by specifying those actions in the XML file. Two sets can be specified, one for the default permissions for community members and another for guest users:
<community-defaults>
            <action-key>ADD_FILE</action-key>
            <action-key>ADD_MESSAGE</action-key>
            <action-key>REPLY_TO_MESSAGE</action-key>
            <action-key>SUBSCRIBE</action-key>
            <action-key>VIEW</action-key>
        </community-defaults>
        <guest-defaults>
            <action-key>VIEW</action-key>
        </guest-defaults>
Note that for this defaults to be applied you have to give a value of true to the addCommunityPermissions and addGuestPermissions parameters when creating the associated resources:
resourceLocalService.addResources(
     test.getCompanyId(), test.getGroupId(), test.getUserId(),
     SCTest.class.getName(), test.getTestId(),
     false, addCommunityPermissions, addGuestPermissions);

Disabling certain permissions for guest #

Some permissions that are considered dangerous can be disabled for guests. By doing this not even the administrator will be able to assign the permissions to users that are not logged in:
<guest-unsupported>
            <action-key>ADD_FILE</action-key>
            <action-key>ADD_SUBCATEGORY</action-key>
            <action-key>SUBSCRIBE</action-key>
            <action-key>UPDATE</action-key>
            <action-key>UPDATE_THREAD_PRIORITY</action-key>
        </guest-unsupported>

Learn more #

http://www.liferay.com/web/guest/community/wiki/-/wiki/Main/Permissioning+Explained
Definitions : :;layout manager: Any user who has editorial rights to the current page/layout. These include Community Administrators, Community Owners, Power Users (only when in their private Community), or any user who has been granted or has inherited UPDATE permission on the current page/layout.

No comments:

Post a Comment