Security and Permissions
Liferay Permission 시스템에 대해서 정리한다. 총 6가지 토픽이 있고 하나씩 보면서 정리해가겠다.
- Using Portal Roles in a Portlet
- Adding Permissions to Resources
- Adding and Deleting Resources
- Exposing the Permission Interface to Users
- Checking Permissions
- Creating a Custom Action Key
1. Using Portal Roles in a Portlet
JSR 286 portlet specification에는 포틀릿에서 사용되는 role을 portlet.xml에 정의한다. 예를 들어 Guestbook project (guestbook, guestbook admin portlet 존재)를 보면 다음과 같다.
<portlet>
<portlet-name>guestbook</portlet-name>
<display-name>Guestbook</display-name>
<portlet-class>
com.liferay.docs.guestbook.portlet.GuestbookPortlet
</portlet-class>
<init-param>
<name>view-template</name>
<value>/html/guestbook/view.jsp</value>
</init-param>
<expiration-cache>0</expiration-cache>
<supports>
<mime-type>text/html</mime-type>
<portlet-mode>view</portlet-mode>
</supports>
<portlet-info>
<title>Guestbook</title>
<short-title>Guestbook</short-title>
<keywords></keywords>
</portlet-info>
<security-role-ref>
<role-name>administrator</role-name>
</security-role-ref>
<security-role-ref>
<role-name>guest</role-name>
</security-role-ref>
<security-role-ref>
<role-name>power-user</role-name>
</security-role-ref>
<security-role-ref>
<role-name>user</role-name>
</security-role-ref>
</portlet>
-> administrator, guest, power-user, user roles이 있음을 알 수 있다. 그리고 이 role들은 portal의 어떤 role과 매핑되어야한다. (다른 포틀릿에서 같은 이름을 가지는 role들과의 conflict를 방지하기 위해 매핑이 필요하다)
이 role mapping은 liferay-portlet.xml에서 해준다
<role-mapper>
<role-name>administrator</role-name>
<role-link>Administrator</role-link>
</role-mapper>
<role-mapper>
<role-name>guest</role-name>
<role-link>Guest</role-link>
</role-mapper>
<role-mapper>
<role-name>power-user</role-name>
<role-link>Power User</role-link>
</role-mapper>
<role-mapper>
<role-name>user</role-name>
<role-link>User</role-link>
</role-mapper>
-> portlet 정의 시 power-user를 레퍼런스하면 이 포틀릿은 Liferay DB에 존재하는 Power User이라는 role에 매핑이 된다. 그리고 이 role이 portal과 매핑되면 getRemoteUser, isUserInRole, getUserPrincipal 메소드를 사용가능하다.
예제)
if (renderRequest.isUserInRole("power-user")){} // to check if the current user has the power-user role
그러나 Liferay는 기본적으로 isUserInRole을 built-in 포틀릿에 사용하지 않는다. Liferay는 내장된 Permission system을 써서 더 fine-grained한 보안을 제공한다. 그래서 포틀릿은 다른 포털 서버에 deploy 할 것이 아니라면 Liferay의 Permission system을 쓰기를 권장한다. (더 강력한 application permission을 제공)
2. Adding Permissions to Resources (D of DRAC)
모든 resources와 actions를 정의하는 부분이다.
top level action, resource level action
default.xml
https://github.com/liferay/liferay-portal/tree/master/portal-impl/src/resource-actions
3. Adding and Deleting Resources (R of DRAC)
resource-action pair를 permission system에 등록하는 부분이다.
용어 정리
Action : Portal 유저에 의해 실행되는 Operation
Resource : Action이 실행되는 portlet이나 entity
Permission : Resource에 실행되는 action
Adding a Resource
Resource는 entity가 database에 추가되는 동시에 같이 추가되어야한다. 이러기 위해 Liferay의 service method를 호출해야 하는데, resource를 추가하는 메소드는 ResourceLocalServiceUtil의 addResource() method이다.
public void addResource(long companyId, long groupId, long userId, String name,
String primKey, boolean portletActions,
boolean addGroupPermissions, boolean addGuestPermissions)
companyId : entity의 portal instance의 primary key
groupId : entity의 site의 primary key
userId : entity를 추가하는 user의 primary key
name : 추가되는 entity의 fully qualified Java class name
primKey : entity의 primary key
portletActions : Portlet action permissions을 추가하기 위해선 true
addGroupPermissions : 현재 group에게 default permission을 설정하려면 true
addGuestPermissions : guest group에게 default permission을 설정하려면 true
Permission-controlled action을 가지는 entity는 반드시 resource로 추가되어야한다. 예를 들어 유저가 guestbook을 추가할 때에는 addResources() method를 통해 해당하는 resource를 resource system에 추가해야한다.
resourceLocalService.addResources(
user.getCompanyId(), groupId, userId, Guestbook.class.getName(),
guestbookId, false, true, true);
-> 위 경우, portletAction을 false로 설정하였는데, 그 이유는 portlet resource가 아닌 model resource가 추가되었기 때문이다. default permission이 적용되어야 하기 때문에 뒤에 두 파라미터는 true로 set되었다.
Liferay JSP에 <liferay-ui:input-permissions />
tag를 사용함으로써 사용자가 default group permission이나 default guest permission을 추가하도록 지원할 수 있다.
Deleting a Resource
Entity를 db에서 지울 때, 그 entity와 매핑되었던 permission도 지워져야한다. 그럼으로써 dead resources가 db의 공간을 차지하는 것을 막을 수 있다. ResourceLocalServiceUtil의 delteResource() method를 이용하여 지울 수 있다.
4. Exposing the Permission Interface to Users
UI를 제공함으로써 portlet의 permission을 configurable하게 만드는 부분이다. Portlet level에서는 portlet에 permission 시스템이 정상작동하게 하기위해 코드를 쓸 필요가 없다. configuration file(default.xml)의 portlet-resource tag에 permission을 정의해놨다면 그것들은 자동으로 Liferay permision UI의 list에 추가된다.
Adding Permissions
model resource에 configurable한 permission을 제공하기 위해서는 permission interface를 UI에 추가해야 한다.
<liferay-security:permissionsURL>
: permission settings configuration page에 URL을 전달
<liferay-ui:icon>
: User에게 icon을 보여준다. 이것들은 theme에 정의되어있고 하나는 permission을 위해 사용된다.
// guestbook_actions.jsp
<liferay-security:permissionsURL
modelResource="<%= Entry.class.getName() %>"
modelResourceDescription="<%= entry.getMessage() %>"
resourcePrimKey="<%= String.valueOf(entry.getEntryId()) %>"
var="permissionsURL" />
<liferay-ui:icon image="permissions" url="<%= permissionsURL %>" />
modelResource : Entity의 fully qualified class name.
modelResourceDescription : model instance를 가장 잘 설명하는 description
resourcePrimkey : entity의 primary key
var : 결과로 나오는 URL이 저장될 변수. 이후에 <icon> tag의 url attribute로 전달된다.
redirect (optional attribute) : upper right arrow link의 default behaviour를 오버라이드 하고 싶을 때 사용
5. Checking Permissions
permission checker를 통해 정상적으로 permission이 작동하는지 체크하는 부분이다. 예를 들어 business layer에서 resource를 지우기 전에 permission check, 혹은 권한이 없는 사용자는 entity를 추가하는 버튼을 숨기는 것들이 해당된다.
Implementing Permissiong Checking
Guestbook portlet의 default.xml한 선언한 action 중 ADD_GUESTBOOK은 소스 코드 내의 UI와 Business logic, 두 군데에서 permission checking을 해야한다. UI는 JSP에 구현되어있고, Business logic은 portlet layer와 service layer에 구현되어있다. JSP file에 대해서는 permission check를 특정 element에 함으로써 권한이 있는 사용자들만 그 element를 볼 수 있다던지의 interaction이 가능하도록 할 것이다 (Add Guestbook button이 권한이 있는 사용자에게만 보이도록). 다음은 ADD_GUESTBOOK action이 permission check에 어떻게 wrap되는지 보이는 예제다.
// view.jsp
<c:if test='<%= GuestbookModelPermission.contains(permissionChecker, scopeGroupId, "ADD_GUESTBOOK") %>'>
// Display the Add Guestbook button
</c:if>
다음은 Business logic에서의 permission check이다. GuestbookPortlet.addGuestbook() method는 GuestbookServiceUtil.addGuestbook() method를 호출하기 때문에 service layer에 permission checking을 구현하면 된다.
GuestbookModelPermission.check(getPermissionChecker(),
serviceContext.getScopeGroupId(), ActionKeys.ADD_GUESTBOOK);
Permission check가 fail이라면 PrincipalException이 던져질 것이고 ADD_GUESTBOOK request는 무시될 것이다. GuestbookModelPermission 헬퍼 클래스는 다음과 같다.
package com.liferay.docs.guestbook.service.permission;
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.security.auth.PrincipalException;
import com.liferay.portal.security.permission.PermissionChecker;
public class GuestbookModelPermission {
public static final String RESOURCE_NAME = "com.liferay.docs.guestbook.model";
public static void check(PermissionChecker permissionChecker, long groupId,
String actionId) throws PortalException {
if (!contains(permissionChecker, groupId, actionId)) {
throw new PrincipalException();
}
}
public static boolean contains(PermissionChecker permissionChecker,
long groupId, String actionId) {
return permissionChecker.hasPermission(groupId, RESOURCE_NAME, groupId,
actionId);
}
}
PermissionChecker는 Liferay class로 hasPermission이라는 method를 가지고 있다. 이 메소드는 어떤 action을 취하려는 user가 적합한 permission이 있는지 체크한다. 만약 로그인하지 않은 guest user라면 guest permission을 체크한다.
// hasPermission method Parameter 설명
groupId : Permission check가 수행되는 scope를 표현한다. Liferay에서는 global (company) scope, group (site) scope,
group template scope, individual scope가 존재한다. 현재 scope의 groupId를 얻는 방법은 두가지가 있다. themeDisplay를
이용하는 방법과 serviceContext를 이용하는 방법이다.
name : default.xml에 정의된 resource의 이름
primKey : Resource의 primary key. Guestbook 예제에서는 resource가 db에 entry로 존재하지 않았기 때문에 groupId를 사용한다.
이미 존재하는 guestbook에 대해 permission을 체크할 때는 guestbook의 primary key를 사용한다.
actionId : default.xml에 있는 action의 이름.
Liferay에서는 자동으로 PermissionChecker instance를 생성한다. PermissionChecker를 얻을 수 있는 방법은 두가지가 있다.
- JSP에서는 <theme:defineObjects/> tag를 사용하면 permissionChecker라는 변수가 생성된다.
- Service Builder를 이용하면 모든 service implementation class는 getPermissionChecker() 메소드를 통해 PermissionChecker instance에 접근할 수 있다.
Service Builder를 이용하지 않는다면 다음과 같은 방법을 쓰면 된다
ThemeDisplay themeDisplay = (ThemeDisplay)
request.getAttribute(WebKeys.THEME_DISPLAY);
PermissionChecker permissionChecker =
themeDisplay.getPermissionChecker();
다음으로는 permission checking을 위한 헬퍼 클래스를 만드는 방법에 대한 것이다. 헬퍼 클래스를 이용하면 permission check를 호출하는 것을 쉽게 할 수 있다. 헬퍼 클래스를 이용하면 PermissionChecker.hasPermission() method를 일일이 호출할 필요가 없어진다.
Creating Helper Classes for Permission Checking
헬퍼 클래스는 permissionChecker와 resource의 이름을 encapsulate 함으로써 코드를 간소하게 해준다. GuestbookModelPermission
은 top-level resource action의 permission을 체크할 때 사용되고 GuestbookPermission
은 Guestbook model resource action의 permission을 체크할 때 사용된다. 여기선 GuestbookPermission
헬퍼 클래스가 JSP에서 사용되는 과정을 보일 것이다.
// JSP
<%
if (GuestbookPermission.contains(
permissionChecker, curGuestbook.getGuestbookId(), "VIEW")) {
// show guestbook data
}
%>
// GuestbookServiceImpl
GuestbookPermission.check(getPermissionChecker(), guestbookId,
ActionKeys.DELETE);
// GuestbookPermission helper class
package com.liferay.docs.guestbook.service.permission;
import com.liferay.docs.guestbook.model.Guestbook;
import com.liferay.docs.guestbook.service.GuestbookLocalServiceUtil;
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.exception.SystemException;
import com.liferay.portal.security.auth.PrincipalException;
import com.liferay.portal.security.permission.PermissionChecker;
public class GuestbookPermission {
public static void check(PermissionChecker permissionChecker,
long guestbookId, String actionId) throws PortalException,
SystemException {
if (!contains(permissionChecker, guestbookId, actionId)) {
throw new PrincipalException();
}
}
public static boolean contains(PermissionChecker permissionChecker,
long guestbookId, String actionId) throws PortalException,
SystemException {
Guestbook guestbook = GuestbookLocalServiceUtil
.getGuestbook(guestbookId);
return permissionChecker
.hasPermission(guestbook.getGroupId(),
Guestbook.class.getName(), guestbook.getGuestbookId(),
actionId);
}
}
GuestbookModelPermission의 메소드의 parameter와 GuestbookPermission의 메소드의 parameter를 비교해보자
GuestbookPermission
- PermissionChecker object
- Action이 수행될 Entity의 primary key
- (Entity에 수행할) Action의 ID
GuestbookModelPermission
- PermissionChecker object
- Action이 수행될 group의 primary key
- (Group에 수행할) Action의 ID
--> GuestbookModelPermission은 top-level action permission을 체크하기 위한 것이고, GuestbookPermission은 resource action permission을 체크하기 위한 것임을 알 수 있다. 그래서 항상 permission helper classes를 만들 때에는 top-level action에 대한 헬퍼 클래스와 custom entity들마다의 헬퍼 클래스를 만들어 줘야한다! 그리고 헬퍼 클래스는 항상 check와 contain method를 포함해야 한다.
Action ID는 permission check에 필요한 action을 지칭할 때 사용된다. 그리고 custom portlet의 ActionKeys.DELETE와 같은 action key를 사용하기를 권장한다.