Tuesday, May 26, 2020

ResourcePermissionの考察 (2)

前回、Liferayが一般ロールに与える権限を考察しました。今回は、サイトロール、組織ロールなどグループロールに対する権限の考察から始めようと思います。Liferay権限システムでは、サイトロールと組織ロールに付与される一般権限の適用範囲はグループテンプレートと認識し、ResourcePermissionテーブルではscope=3になります

それでは、前回と同じく、モデルリソースとポートレットリソース別でサイトロール、組織ロールに付与する権限を考察しましょう。

サイトロール x モデルリソース

以下の準備を行いましょう。設定方法は前回同様です。

  • blog_site_roleサイトロールを作成する
  • blog_site_roleを開き
    • ブログ/エントリー追加する権限を追加する
    • ブログのエントリ/表示権限を追加する
  • 前回、デフォルトサイトに作成したブログエンティティpermission-blogの権限定義において
    • blog_site_role更新権限を追加する
では、Role_ResourcePermissionテーブルを確認しましょう。

select roleId, name, type_ from Role_ where name = "blog_site_role";
select resourcePermissionId, name, scope, primKey, primKeyId, roleId, actionIds
from ResourcePermission where roleId = 50910;
結果は以下の通りです。

roleId ロール名 ロールtype
50910 blog_site_role 2
ID リソース名 scope primKey primKeyId roleId 操作
5226 com.liferay.blogs 3 0 0 50910 2
5227 com.liferay.blogs.model.BlogsEntry 3 0 0 50910 1
5228 com.liferay.blogs.model.BlogsEntry 4 50893 50893 50910 32
その結果が表す意味を考察しましょう。

ID ロール どんなリソース どの操作
5226 blog_site_role scope=3: サイトロールblog_site_roleを持つサイト中の全てcom.liferay.blogs ADD_ENTRY
5227 blog_site_role scope=3: サイトロールblog_site_roleを持つサイト中の全てcom.liferay.blogs.model.BlogsEntry VIEW
5228 blog_site_role scope=4: id=50893com.liferay.blogs.model.BlogsEntryインスタンス UPDATE
※ ユーザは複数サイト中同じサイトロールに割り当てられることができるため、ユーザがサイトロールを持つサイト中のみに、そのサイトロールに与えるscope=3の権限を持ちます。

サイトロール x ポートレットリソース

以下の準備を行いましょう。
  • blog_site_roleを開いて、権限定義に
    • アプリケーション権限/ページに追加する権限を追加する
  • デフォルトサイトのブログポートレットに
    • ポートレット権限設定を開いてblog_site_role設定権限を追加する
それ後、ロールとResourcePermissionテーブルで確認しましょう。結果は以下の通りです(重複するデータについては除外します)。


ID リソース名 scope primKey primKeyId roleId 操作
5232 com_liferay_blogs_web_portlet_BlogsPortlet 3 0 0 50910 2
5233 com_liferay_blogs_web_portlet_BlogsPortlet 4 38656_LAYOUT_com_liferay_blogs_web_portlet_BlogsPortlet 0 50910 4
結果を考察しましょう。

ID ロール どんなリソース どの操作
5232 blog_site_role scope=3: サイトロールblog_site_roleを持つサイト中の全てBlogsPortlet ADD_ENTRY
5228 blog_site_role scope=4: id=38656のレイアウト上のBlogsPortlet UPDATE

組織ロール

Liferayでは組織ロールの挙動はサイトロールとほぼ一致のため省略します。

まとめ

ここまで考察した結果をまとめましょう。scope=3のサイトロールと組織ロールの適用範囲はユーザが当該ロールを割り当てられているかとうかで決まります。

  • 特定インスタンスを持っていないリソース
    • サイトロールの権限の適用範囲は、ユーザが当該ロールを割り当てられているサイトのみです。そのため、リソースprimKeyは0になります。
    • 組織ロールの権限の適用範囲は、ユーザが当該ロールを割り当てらている組織の組織サイトです。そのため、リソースprimKeyは0になります。
  • リソースの特定インスタンス
    • 権限の適用範囲はnameprimKeyが特定できるリソースのみです。

リソース種類 特定インスタンス リソース名 scope primKey 適用範囲
モデル N モデル名 1 companyId primKeyが表すLiferayインスタンス
モデル N モデル名 3 0 roleIdが表すロールを持ち場所(サイトまたは組織のサイト)中の全モデルインスタンス
モデル Y モデル名 4 リソースId primKeyが表すモデルインスタンス
ポートレット N ポートレットキー 1 companyId primKeyが表すLiferayインスタンス
ポートレット N ポートレットキー 3 0 roleIdが表すロールを持ち場所(サイトまたは組織のサイト)中の全primKeyが表すポートレット
ポートレット Y ポートレットキー 4 layout
ポートレットキー
primKeyが表すポートレット

その他

チームロール
Liferayでは、サイト内のチームが作成できます。チームに権限を与えることもできます。例として、デフォルトサイトにteam-1チームを作成した後に、前回作成したpermission-blogの権限設定を開いてteam-1ロールを確認できます。

では、team-1ロールに更新する権限を追加し、データベースを確認しましょう。

select teamId, name from Team where name = "team-1";
select roleId, name, type_ from Role_ where name = "50924";
select resourcePermissionId, name, scope, primKey, primKeyId, roleId, actionIds
from ResourcePermission where roleId = 50925;
結果は以下の通りです。

teamId チーム名
50924 team-1
roleId ロール名 ロールtype
50925 50924 4
ID リソース名 scope primKey primKeyId roleId 操作
5239 com.liferay.blogs.model.BlogsEntry 4 50893 50893 50925 32
チームを作成後、Liferayは自動的にclassPk=teamIdclassName=チームのロールを作成します。そのロールに与える権限はチームの所属サイト上のブログインスタンスとブログポートレットインスタンスだけのため、チームロールの権限レコードはscope=4になります。
scope=2
Liferay公式ブログ記事の説明の通り、scope=2(グループ範囲)のResourcePermissionレコードが存在します。ただし、今まで検証した権限のscopeはいずれでも2になりません。では、scope=2の権限は一体何でしょうか?

答えは、指定されたグループ(=サイト)内のリソースのみに有効する権限です。以下の手順で検証しましょう。

  • 一般ロールblog_roleを開き
    • 権限の定義ブログ/権限設定を追加する
    • 追加の際、権限設定項目の右の変更ボタンを押し、Liferay DXP(デフォルトサイト)とユーザー非公開サイトを選択する
その後、データベースをチェックしましょう。

select resourcePermissionId, name, scope, primKey, primKeyId, roleId, actionIds
from ResourcePermission where roleId = 50867 and scope = 2;
結果はご覧の通り、scope=2primKeyLiferay DXPサイトとユーザー非公開サイトgroupIdの権限レコードが作成されました。

ID リソース名 scope primKey primKeyId roleId 操作
5324 com_liferay_blogs_web_portlet_BlogsPortlet 2 20126 20126 50867 8
5325 com_liferay_blogs_web_portlet_BlogsPortlet 2 20132 20132 50867 8
では、scope=2の権限とscope=3のサイトロールの権限の違いを考察しましょう。

scope 適用範囲
2 primKeyが表すサイト内のリソース、1個レコードの適用サイト数は1
3 roleIdが表すサイトロールを持つサイト内のリソース 、1個レコードは複数サイト適用可能

次回はここまて考察した内容をLiferayカスタマイズでの運用を考察したい思います。

Wednesday, May 20, 2020

ResourcePermissionの考察 (1)

こんにちは。ウです。

みな様がLiferayカスタマイズをする時、権限周りのメソッド"シグネチャーに困ったことがありますか?例えば、PermissionCheckerとか、Liferay 7.1から新規したヘルパークラスModelResourcePermission.javaPortletResourcePermission.javaなどにおいてはメソッド中、このような定義がよく見えます。

//PermissionChecker
public boolean hasOwnerPermission(long companyId, String name, long primKey, long ownerId, String actionId);

public boolean hasPermission(Group group, String name, long primKey, String actionId);

// ModelResourcePermission
public void check(PermissionChecker permissionChecker, long primaryKey, String actionId);

public void check(PermissionChecker permissionChecker, T model, String actionId);

// PortletResourcePermission
public void check(PermissionChecker permissionChecker, Group group, String actionId);

どの場面でどのメソッドを利用しますか?利用の際どの値をメソッドに与えますか?そのメソッドシグネチャーはLiferayの権限管理の関係はどうですか?今回はこの権限周りにいて考察しようと思います。

ちなみに、みなさまはLiferayオフィシャルブログの権限関連記事を読んだことがありますか?この記事はLiferayのロールと権限の基本関係を説明しているのでぜひおすすめです。

Liferayの権限

みな様のご存知通り、Liferayの権限システムには、ポートレットリソースまたはモデルリソース毎の粒度で行われています。従って:

  • Liferayリソース(BlogJournalArticleまたはDLFileEntryなど)の初期パーミッションは全部ResourceLocalService.addResourcesに介してResourcePermissionテーブルにレコードを作成している(例:JournalArticleLocalServiceImpl.java)。
  • 逆に、PermissionCheckerがユーザの権限をチェックする際、同じくResourceLocalService.hasUserPermissionなどのメソッドを利用している(例:AdvancedPermissionChecker.java)。

すなわち、Liferayでは、ResourcePermissionPermissionCheckerを利用し本当のResourcePermissionテーブルの詳細情報を隠しています。Liferayの権限システムを深く理解したい場合、ResourcePermissionテーブルとリソース権限の関係を考察しないといけない理由はそこにあります。

ResourcePermissionテーブル

Liferayが行なっている権限管理はロールベースアクセス制御(RBAC)方式のため、最終的にデータベースに記入するレコードは以下の内容になります:
  • あるロールが
  • あるリソースに対して
  • どんな操作が許可される
ResourcePermissionテーブルはその三つの情報を含んでいます。上記Liferayオフィシャルブログ記事を参考して具体的に整理してみましょう。

  • name: リソース名
    • ポートレットリソース:ポートレットキー
    • モデルリソース:モデル名
  • primKey: リソースID(権限範囲)
    • 全Liferayインスタンス有効権限: companyId
    • グループ内有効: groupId
    • 具体的なリソースに対する権限:
      • モデルリソース: リソースprimKey
      • ポートレットリソース: layoutId + _LAYOUT_ + portletId
  • scope: 権限の範囲
    • 1: 全Liferayインスタンスに有効
    • 2: グループ内のみ有効
    • 3: グループテンプレート(サイト、組織)内のみ有効
    • 4: 具体的なリソースインスタンスに対する権限
  •  roleId: ロールID
  •  actionId: 権限が許可する操作

まとめると、以下のグラフのようにRBACに必要な情報をレコードで格納しています。


それでは、具体的にどのような内容が格納されるのかを確認しましょう。

考察

今回はLiferayの標準モジュールBlogポートレットを利用してResourcePermissionテーブル中一般ロールに対する権限の表現を確認しましょう。

一般ロール x モデルリソース

以下の準備を行いましょう:

  • 一般ロールblog_roleを作成する
  • blog_roleを開いて、権限定義に
    • ブログ/エントリー追加する権限を追加する
    • ブログのエントリ/表示権限を追加する


  • デフォルトサイトにブログポートレットを置き、その中にpermisison-blogというブログエンティティを作成する
  • permission-blogの権限定義に、
    •  blog_role表示更新を追加する

その後、ロールとResourcePermissionテーブルで確認しましょう。
select roleId, name, type_ from Role_ where name = "blog_role";
select resourcePermissionId, name, scope, primKey, primKeyId, roleId, actionIds from ResourcePermission where roleId = 50867;

結果は以下の通りです。
roleId ロール名 ロールtype
50867 blog_role 1
ID リソース名 scope primKey primKeyId roleId 操作
5207 com.liferay.blogs 1 20099 20099 50867 2
5218 com.liferay.blogs.model.BlogsEntry 1 20099 20099 50867 1
5219 com.liferay.blogs.model.BlogsEntry 4 50893 50893 50867 33

では、その結果はどのような権限を表すのかについて考察しましょう。
ID ロール どんなリソース どの操作※
5207 blog_role scope=1: liferayインスタンス20099中の全てcom.liferay.blogs ADD_ENTRY
5218 blog_role scope=1: liferayインスタンス20099中の全てcom.liferay.blogs.model.BlogsEntry VIEW
5219 blog_role scope=4: id=50893com.liferay.blogs.model.BlogsEntryインスタンス VIEW + UPDATE

一般ロール x ポートレットリソース

続いて、以下の準備を行いましょう。
  •  blog_roleを開いて、権限定義に
    • アプリケーション権限/ページに追加する権限を追加する

  • デフォルトサイトのブログポートレットに
    • ポートレット権限設定を開いてblog_role設定権限を追加する

それ後、ロールとResourcePermissionテーブルで確認しましょう。
select resourcePermissionId, name, scope, primKey, primKeyId, roleId, actionIds from ResourcePermission where roleId = 50867;

結果は以下の通りです(重複レコードを除外します)。
ID リソース名 scope primKey primKeyId roleId 操作
5222 com_liferay_blogs_web_portlet_BlogsPortlet 1 20099 20099 50867 2
5223 com_liferay_blogs_web_portlet_BlogsPortlet 4 38656_LAYOUT_com_liferay_blogs_web_portlet_BlogsPortlet 0 50867 4

では、その結果はどのような権限を表すのかについて考察しましょう。
ID ロール どんなリソース どの操作
5222 blog_role scope=1: liferayインスタンス20099中の全てBlogsPortlet ADD_TO_PAGE
5219 blog_role scope=4: id=38656のレイアウト上のBlogsPortlet CONFIGURATION

まとめ

ここまでチェックすると、権限を与えられるリソースは三種類があることがわかりました。
  • 具体的なインスタンスを持っていないリソース
    • そのモデルクラスは存在しないためインスタンス化できないため仮想モデルとして認識します。
      • 例:リソースcom.liferay.blogsはLiferayブログその概念を表すものと認識できます。
    • そのリソースに与えられる権限は、具体的なブログエンティティに関係ありません。
      • 権限例:新規作成、パーミッション設定、ブログの購読
    • 特定できるインスタンスに適用しないため、scopeは4になれません。
    • primKeyは権限が適用される範囲の対象を表します。
      • scope=1の場合、primKeyはliferayインスタンスIDです。
  • 任意ある種類のインスタンス化可能なリソース
    • 対象は任意Liferayモデルまたはポートレット
      • 例:リソースcom.liferay.blogs.model.BlogsEntry, scope=1は全Liferayインスタンス中の任意Liferayブログを表し、そのリソースに与える権限は全Liferayインスタンス中のブログで有効となります。
      • 例:リソースcom_liferay_blogs_web_portlet_BlogsPortlet, scope=1は全Liferayインスタンス中任意ブログポートレットを表し、そのリソースに与える権限は全Liferayインスタンス中のブログポートレットで有効となります。
    • 操作は具体的にインスタンスに対するアクションです。
      • 例:参照、更新、削除、コメント追加、ポートレット追加
    • 任意インスタンスに適応する権限のため、scopeは4になれません。
    • primKeyは権限が適用される範囲の対象を表します。
      • scope=1の場合、primKey=LiferayインスタンスID
  • 特定したリソース
    • 対象は任意Liferayモデルまたはポートレット
      • 例:リソースcom.liferay.blogs.model.BlogsEntry, scope=4primKeyに指定するブログエンティティと認識します。その権限はその特定のブログエンティティのみで有効です。
      • 例:リソースcom_liferay_blogs_web_portlet_BlogsPortlet, scope=4primKeyに指定するブログポートレットと認識します。その権限はその特定のブログポートレットので有効です。
    • 操作は具体的にインスタンスに対するアクションです。
    • primKeyは特定インスタンスのIDになります。
      • モデルリソースの場合、primKey=モデルID
      • ポートレットリソースの場合、primKey=layoutId + _LAYOUT_ + ポートレットキー
次回、サイトロールなどグループロールに与える権限を考察しようと思います。


※ actionIdの内容は以下のsqlで取得できます。
select name, actionId, bitwiseValue from ResourceAction where name="com.liferay.blogs" and bitwiseValue = 2;
select name, actionId, bitwiseValue from ResourceAction where name="com.liferay.blogs.model.BlogsEntry" and (bitwiseValue = 1 or bitwiseValue=32);
select name, actionId, bitwiseValue from ResourceAction where name="com_liferay_blogs_web_portlet_BlogsPortlet " and (bitwiseValue = 2 or bitwiseValue = 4);
リソース名操作bitwiseValue
com.liferay.blogsADD_ENTRY2
com.liferay.blogs.model.BlogsEntryVIEW1
com.liferay.blogs.model.BlogsEntryUPDATE32
com_liferay_blogs_web_portlet_BlogsPortletADD_TO_PAGE2
com_liferay_blogs_web_portlet_BlogsPortletCONFIGURATION4

Friday, May 1, 2020

Tomcat9をアップデートしたらWebサーバ(Apache)とAJP接続できなくなった話

こんにちは。おおたにです。

先日Tomcat9を最新バージョン(その時点では9.0.33)にアップデートしたところ、WebサーバとのAJP接続がうまくいかなくなったので、今回はその対処法を紹介します。

原因


こちらの脆弱性への対応のため、Tomcat 9.0.31でAJP1.3コネクタ設定が変更されたことが原因でした。具体的な変更点はTomcat 9.0.31のchangelogにありますが、主なものは以下の3点です。
  • AJPコネクタ(8009番ポートのやつ)がデフォルトでdisabledになった
  • バインドアドレスのデフォルトがIPv6ループバックアドレス(::1)になった
  • 新しい属性secretRequiredが追加され、デフォルトでtrueとなっている(trueの場合、secret属性でシークレットキーを指定する必要がある)

以下、上記変更に対応するための設定方法です。今回はApache HTTP Serverを例に説明しますが、Apache HTTP Serverのバージョンにより設定が若干異なります。

Apache HTTP Server 2.4.43以降の場合


Tomcat側は、<TOMCAT_DIR>/conf/server.xmlでAJPコネクタのコメントアウトを外した後、以下のように設定します。
    <Connector protocol="AJP/1.3"
               address="TomcatサーバのIPアドレス"
               port="8009"
               secret="シークレットキー"
               redirectPort="8443" URIEncoding="UTF-8" />
  • address : TomcatサーバのIPアドレス。複数のインタフェースが存在する場合はWebサーバとの通信に利用する方のIPアドレスを指定する。
  • secret : AJPコネクタのシークレットキー。

Apache HTTP Server側は、ProxyPass設定にsecretパラメータを追加して先ほどと同じ値を指定します。
ProxyPass / ajp://TomcatサーバのIPアドレス/ secret=シークレットキー

Apache HTTP Server 2.4.41以前の場合


AJPコネクタのシークレットキーに対応できないため、Tomcat側でsecretRequiredfalseに設定する必要があります。
    <Connector protocol="AJP/1.3"
               address="TomcatサーバのIPアドレス"
               port="8009"
               secretRequired="false"
               redirectPort="8443" URIEncoding="UTF-8" />
Apache HTTP Server側は特に設定変更しなくてOKです。


弊社ではLiferayやAlfrescoといったオープンソース製品のカスタマイズ/導入支援を行っておりますが、Tomcat/Apacheを使ってホストすることが多いためさっそくこの問題にあたりました。どうぞご参考まで。

Friday, April 24, 2020

ユーザ、グループと組織の関係に役立つ便利なツール(その2)

前回、ユーザからユーザの所属または所有ロールを取得するための便利なUserBagを紹介しました。今回は、その逆方向、組織の下の全てユーザ、あるロールにアサインした全てのユーザ、またはサイトメンバー全員を取る方法を紹介しようと思います。

実は、Liferayのポータル機能として、ユーザ管理、ロール管理またはサイトメンバー一覧などの画面で、すでに上記機能が実装されていますが、その画面が利用したメソッドは、ポートレットからの呼び出しを前提としたシグネチャーを持っています(すなわち、PortletRequestHttpServletRequestなど)。 Liferayカスタマイズの際、サービスレイヤからそういう機能を実装する場面が多いため、今回の内容は、Liferayの既存機能の中のxxxRequestなどポートレット特化のパラメーターを利用しないこととします(すなわち本来xxxRequestから作成したものをハードコードで作成する)。

UserSearch

LiferayのUsersAdminPortletまたはSiteMembershipsPortletのユーザリスト部分を確認するとユーザ管理のViewUsersManagementToolbarDisplayContext.javaまたはサイトメンバー管理のUsersDisplayContext.java、よくUserSearchというクラスを見ます。さらに、Liferayには、階層構造を用いてデータベースの検索よりサーチエンジンの検索が早いことを利用し、指定条件のユーザを取得できるメソッドが用意されています。

// UsersDisplayContext.java
UserSearch userSearch = new UserSearch(_renderRequest, getPortletURL());
UserSearchTerms searchTerms =
 (UserSearchTerms)userSearch.getSearchTerms();

LinkedHashMap userParams = new LinkedHashMap<>();
userParams.put("inherit", Boolean.TRUE);
userParams.put("usersGroups", Long.valueOf(getGroupId()));

int usersCount = UserLocalServiceUtil.searchCount(
 themeDisplay.getCompanyId(), searchTerms.getKeywords(),
 searchTerms.getStatus(), userParams);

userSearch.setTotal(usersCount);

List users = UserLocalServiceUtil.search(
 themeDisplay.getCompanyId(), searchTerms.getKeywords(),
 searchTerms.getStatus(), userParams, userSearch.getStart(),
 userSearch.getEnd(), userSearch.getOrderByComparator());

userSearch.setResults(users);
そして、UserSearch.javaUserSearchTerms.javaなどのソースを参照し、以下のようなサービスレイヤ向けUserSearchが作成できます。
  • searchTerms.getKeywords()のデフォルト値はnullになる
  • ユーザ全体を取りたい場合、start=0end=総数を指定する
  • userSearch.getOrderByComparator()のデフォルト値はUsersAdminUtil.getUserOrderByComparator("last-name", "asc")になる
Component(
 property = {
  "osgi.command.scope=liferay",
  "osgi.command.function=checkOrganizationUsers",
  "osgi.command.function=checkSiteUsers",
  "osgi.command.function=checkRoleUsers"
 },
 service = SiteUserOrgCmd.class
)
public class SiteUserOrgCmd {

 private static final long companyId = PortalUtil.getDefaultCompanyId();

 public void checkOrganizationUsers(String orgName) throws Exception {

  Organization org = OrganizationLocalServiceUtil.getOrganization(companyId, orgName);

  LinkedHashMap userParams = new LinkedHashMap();
  userParams.put("inherit", Boolean.TRUE);
  userParams.put("usersOrgs", org.getOrganizationId());
  int usersCount = UserLocalServiceUtil.searchCount(companyId, StringPool.BLANK, WorkflowConstants.STATUS_APPROVED, userParams);
  OrderByComparator obc = UsersAdminUtil.getUserOrderByComparator("last-name", "asc");
  List res = UserLocalServiceUtil.search(companyId, StringPool.BLANK, WorkflowConstants.STATUS_APPROVED, userParams, 0, usersCount, obc);

  System.out.println("Users of organization " + orgName + ": ");
  for (User u : res) {
   System.out.println("  " + u.getScreenName());
  }

 }

 public void checkSiteUsers(String siteName) throws Exception {

  Group grp = null;
  for (Group g : GroupLocalServiceUtil.getGroups(QueryUtil.ALL_POS, QueryUtil.ALL_POS)) {
   if (g.getName(Locale.getDefault()).equals(siteName)) {
    grp = g;
    break;
   }
  }

  LinkedHashMap userParams = new LinkedHashMap();
  userParams.put("inherit", Boolean.TRUE);
  userParams.put("usersGroups", grp.getGroupId());
  int usersCount = UserLocalServiceUtil.searchCount(companyId, StringPool.BLANK, WorkflowConstants.STATUS_APPROVED, userParams);
  OrderByComparator obc = UsersAdminUtil.getUserOrderByComparator("last-name", "asc");
  List res = UserLocalServiceUtil.search(companyId, StringPool.BLANK, WorkflowConstants.STATUS_APPROVED, userParams, 0, usersCount, obc);

  System.out.println("Users of site " + siteName + ": ");
  for (User u : res) {
   System.out.println("  " + u.getScreenName());
  }

 }

 public void checkRoleUsers(String roleName) throws Exception {

  Role role = RoleLocalServiceUtil.getRole(companyId, roleName);

  LinkedHashMap userParams = new LinkedHashMap();
  userParams.put("inherit", Boolean.TRUE);
  userParams.put("usersRoles", role.getRoleId());
  int usersCount = UserLocalServiceUtil.searchCount(companyId, StringPool.BLANK, WorkflowConstants.STATUS_APPROVED, userParams);
  OrderByComparator obc = UsersAdminUtil.getUserOrderByComparator("last-name", "asc");
  List res = UserLocalServiceUtil.search(companyId, StringPool.BLANK, WorkflowConstants.STATUS_APPROVED, userParams, 0, usersCount, obc);

  System.out.println("Users of site " + roleName + ": ");
  for (User u : res) {
   System.out.println("  " + u.getScreenName());
  }
 }
}

検証


前回と同じ、検証用エンティティを作成しましょう。

  • ユーザ: ub_user, ub_user2, ub_user3
  • ユーザグループ: ub_grp
  • 組織: ub_org
    • ub_orgの下の階層にob_org1を作成する
  • サイト: ub_site
  • 一般ロール: ub_role

では、上記サービスでUserSearchの効果を検証しましょう。

組織

  •  ub_org <- ub_user2ub_org1 <- ub_user3
g! checkSiteUsers ub_site
Users of site ub_site:
  test
  ub_user
  ub_user3
  ub_user2

  • 前回同様、dxp-14パッチまでのLiferayには、ub_user3を直接にub_orgにアサインしなくで、その下のub_org1にアサインすると、ub_user3ub_siteのサイトメンバーとして認識できなくなります。

ロール

ub_roleを:
  • ub_userにアサインする
  • ub_grpにアサインし、ub_user2ub_grpにアサインする
  • ub_orgにアサインし、ub_user3ub_orgにアサインする
g! checkRoleUsers ub_role
Users of site ub_role:
  ub_user
  ub_user3
  ub_user2

まとめ

今回、Liferayが提供するUserSearchを利用し、サイト、組織またはロールの所属ユーザを全部取得する方法を紹介しました。実は、UserSearchだけではなく、RoleSearchUserGroupSearchGroupSearchなどのsearch系メソッドがLiferay中に定義されています。このようなクラスを同じ手段で改造したら、サービスレイヤであるエンティティーの下のロール、ユーザグループの取得も可能になります。

Tuesday, April 21, 2020

ユーザ、グループと組織の関係に役立つ便利なツール(その1)

Liferayカスタマイズのビジネスロジックに、以下のような処理がよくあると思います。


  • ユーザにアサインしたロールを取得する
  • ユーザの所属組織を取得する
  • ユーザの所属サイトを取得する
  • 組織に所属するユーザを取得する
  • サイトのサイトメンバーを取得する
  • など...


みなさまはこのような処理を行う時、userLocalService.getGroupUsers(グループのユーザを取る)、organizationLocalService.getUserOrganizations(ユーザが所属する組織を取得)などのメソッドをよく利用しますか?実は、これは間違っています。

その原因は、Liferayのサービスから取得できるものは、データベースの中に直接存在するレコードのみです。ただし、組織、サイトは階層構造のため、ユーザがサイトに所属する際、ユーザ-サイトに直接関連するデータベースレコードが存在しないこともできます(例えば、ユーザが所属するユーザグループがサイトメンバとしてサイトにアサインすることができます)。

そのため、データベース上直接関連がないエンティティを取るため、再帰的に階層構造をトラバースしないといけません。そこで、Liferayは階層構造を配慮した便利なツールを提供しています。

今回はユーザからユーザの所属サイト、ユーザグループ、組織またはユーザにアサインしたロールを取るツールを紹介したいと思います。次回は逆方向でサイト、ユーザグループ、組織から所属ユーザを取るツールです。

UserBag

以下のコードでUserBagが作成できます。

UserBag userBag = UserBagFactoryUtil.create(userId);

作成されたUserBagを利用し、以下の処理を行えます。

  • userBag.getRoles()
    • ユーザにアサインしたロールを取得する。
  • userBag.getUserUserGroupIds()
    • ユーザが所属するユーザグループのIDを取得する
  • userBag.getUserOrgs()
    • ユーザが所属する組織を取得する。
  • userBag.getUserGroups()
    • ユーザが所属するサイトを取得する。
  • userBag.getGroups()
    • ユーザが所属するグループを取得する。
※ Liferayには、ユーザグループ、サイトと組織その三つのエンティティに全部groupと関連しています。そのため、getUserGroups()メソッドはユーザグループではなく、ユーザの所属グループをリターンします。
※ 実に、ユーザグループと組織に繋がるgroupは、ユーザグループまたは組織のプロファイルページと見られます。

検証

さて、Liferayを起動してUserBagの効果を検証しましょう。ひとまず、検証用モジュールを作りましょう。

@Component(
 property = {
  "osgi.command.scope=liferay",
  "osgi.command.function=checkUserOrganizations",
  "osgi.command.function=checkUserSites",
  "osgi.command.function=checkUserRoles"
 },
 service = SiteUserOrgCmd.class
)
public class SiteUserOrgCmd {

 private static final long companyId = PortalUtil.getDefaultCompanyId();

 public void checkUserOrganizations(String screenName) throws Exception {

  User user = UserLocalServiceUtil.getUserByScreenName(companyId, screenName);
  UserBag userBag = UserBagFactoryUtil.create(user.getUserId());

  System.out.println("User's organizations in userBag: ");
  for (Organization org : userBag.getUserOrgs()) {
   System.out.println("  " + org.getName());
  }
  System.out.println("User's organizations in service: ");
  for (Organization org : OrganizationLocalServiceUtil.getUserOrganizations(user.getUserId())) {
   System.out.println("  " + org.getName());
  }
 }

 public void checkUserSites(String screenName) throws Exception {

  User user = UserLocalServiceUtil.getUserByScreenName(companyId, screenName);
  UserBag userBag = UserBagFactoryUtil.create(user.getUserId());

  System.out.println("User's sites: ");
  for (Group grp : userBag.getUserGroups()) {
   System.out.println("  " + grp.getName(Locale.getDefault()));
  }

  System.out.println("User's sites in service: ");
  for (Group grp : GroupLocalServiceUtil.getUserGroups(user.getUserId(), true)) {
   System.out.println("  " + grp.getName(Locale.getDefault()));
  }

 }

 public void checkUserRoles(String screenName) throws Exception {

  User user = UserLocalServiceUtil.getUserByScreenName(companyId, screenName);
  UserBag userBag = UserBagFactoryUtil.create(user.getUserId());

  System.out.println("User's roles: ");
  for (Role r : userBag.getRoles()) {
   System.out.println("  " + r.getName());
  }

  System.out.println("User's roles in service: ");
  for (Role r : RoleLocalServiceUtil.getUserRoles(user.getUserId())) {
   System.out.println("  " + r.getName());
  }

 }
}

※ Liferayユーザグループが階層構造になれないため、検証内容から外します。

続いて、検証用エンティティを用意します。
- ユーザ: ub_user
- ユーザグループ: ub_grp
- 組織: ub_org
  - 階層組織: ub_org/ub_org1
- サイト: ub_site
- 一般ロール: ub_role

それては、gogo shellでUserBagの効果を検証しましょう。
※ 検証中、明記以外の場合、「検証の度にユーザ、ロール、サイトと組織の関係を元に戻す」を前提として検証操作をします。

組織

以下の状況を考えてみましょう。
  •  ub_org <- ub_user
    • ub_orgは階層構造中一番トップな組織ため、UserBagOrganizationLocalServiceの戻り値は同じです。
g! checkUserOrganizations ub_user
User's organizations in userBag:
  ub_org
User's organizations in service:
  ub_org

  • ub_org1 <- ub_user
    •   ub_org1ub_orgの一個下の階層の組織のため、ユーザが直接にub_org1のユーザとして追加すると、OrganizationLocalServiceはDBに直接存在するレコードub_org1をリターンします。それに対して、UserBagは階層構造の上の組織ub_orgもリターンします。
g! checkUserOrganizations ub_user
User's organizations in userBag:
  ub_org
  ub_org1
User's organizations in service:
  ub_org1

サイト

以下の状況を考えてみましょう。
  • ub_site <- ub_user
    • ユーザが直接にサイトメンバーとしてサイトへ登録すると、UserBagGroupLocalServiceの出力が同じです。
g! checkUserSites ub_user
User's sites:
  ub_site
User's sites in service:
  ub_site

  • ub_site <- ub_grp, ub_grp <- ub_user
    • ユーザが直接にサイトへ登録ではなく、ユーザグループに通じでサイトメンバーへ登録際、UserBagGroupLocalServiceの出力が同じです。
  • 最初に「あれ?」と思う方がいらっしゃいませんか?
    • 実は、GroupLocalServiceUtil.getUserGroups(long userId, boolean inherit))のパラメーターとしてinherit=trueを指定したら、GroupLocalServiceでもUserBagのような出力ができます。
g! checkUserSites ub_user
User's sites:
  ub_site
User's sites in service:
  ub_site

  • ub_site <- ub_org, ub_org <- ub_user
    • ユーザが直接にサイトへ登録ではなく、ユーザグループに通じでサイトメンバーへ登録際、UserBagGroupLocalServiceの出力が同じです。
    • 理由はユーザグループの場合と同じです。
    • ちなみに、組織サイトも結果に入りました。
g! checkUserSites ub_user
User's sites:
  ub_org
  ub_site
User's sites in service:
  ub_org
  ub_site


  • ub_site <- ub_org, ub_org1 <- ub_user
    • ちょっと変な結果ですが、ub_userを直接に組織ub_orgの下に置かなくて、ub_org下のub_org1にアサインしたとき、ub_userub_siteのサイトメンバーとして認識されないようです。
    • これはLiferayの仕様(liferay-7.1-sp2, dxp-14-7110まで)だそうです(サイトメンバー管理画面にも同じ結果が確認できます)。
g! checkUserSites ub_user
User's sites:
  ub_org1
User's sites in service:
  ub_org1

ロール

以下の状況を考えてみましょう。
  • ub_role <- ub_user
    • ユーザが直接にロールをアサインするとき、UserBagRoleLocalServiceの出力が同じです。
g! checkUserRoles ub_user
User's roles:
  User
  ub_role
User's roles in service:
  User
  ub_role

  • ub_role <- ub_grp, ub_grp <- ub_user
    • ユーザがユーザグループを通じてユub_roleにアサインされるとき、データベースにレコードがいないため、UserBagRoleLocalServiceの出力が違います。
g! checkUserRoles ub_user
User's roles:
  User
  ub_role
User's roles in service:
  User

  • ub_role <- ub_org, ub_org1 <- ub_user
    • ユーザが組織を通じてユub_roleにアサインされるとき、データベースにレコードがいないため、UserBagRoleLocalServiceの出力が違います。
    • ub_org1は階層構造上ub_orgの下の階層のため、ユーザはub_roleを持っています。
g! checkUserRoles ub_user
User's roles:
  User
  ub_role
User's roles in service:
  User

まとめ

今回は、Liferayが提供したUserBagを利用し、ユーザから当該ユーザの所属とロールを取得する方法を紹介しました。UserBagはLiferayの階層構造を配慮して結果をリターンするため、カスタマイズの際役に立つでしょう。また、公式github上のソースコードを参考すると、UserBagをより深く理解できますので(UserBagImpl, UserBagFactoryImpl)、ぜひおすすめします。

ちなみに、UserBagの定義はLiferayのkernelモジュールであるcom.liferay.portal.security.permissionの中にあり、LiferayのpermissionCheckerはよくこのクラスを利用しています。これは本来UserBagを開発する理由でしょう。

Wednesday, April 15, 2020

Alfresco Content Applicationを使ってみよう

こんにちは。てらしたです。

今回はAlfrescoの新しいUIとして開発されているAlfresco Content Applicationをご紹介します。Alfresco Content Applicationは、Alfresco Development Framework(ADF)を使って開発されており、Alfresco上のコンテンツの操作に特化したUIとなっています。GitHubのページに記載されているコンセプトがわかりやすかったので引用しておきます。
The concept of this application is a simple user interface which makes accessing files in the Alfresco Content Services repository easy.
Often Content Management systems provide more capabilities out of the box than most users need; providing too many capabilities to these users prevents them from working efficiently, so they may end up using unsanctioned file management solutions which presents a proliferation of content storage and collaboration solutions as well as compliance issues for organizations.
This application simplifies the complexity of Content Management and provides comprehensive extensibility features for developers, using the Alfresco Application Development Framework, to easily and quickly create custom solutions for specific user cases.
Alfrescoは3.x以降、Alfresco Shareを標準のUIとして新機能の実装や既存機能の改善が行われてきました。その結果、機能が多くなり過ぎて全部乗せのようなUIになってしまっています。ダッシュボード、ブログ、Wiki、カレンダーやワークフローは使わず、コンテンツ管理機能だけ使いたいという場合にはShareではtoo muchなので、Alfresco Content Applicationをカスタマイズして使うというケースが今後増えてくるのではないかと思います。

前置きが長くなりましたが、Alfresco Content Applicationを起動する手順をご紹介します。準備さえできてしまえば本当に簡単に起動できます。
まず準備として、最新のAlfrescoをインストールして起動します。現時点(2020年4月)では201911GAが最新のGA版なので、例えばこちらの記事を参考にしてもらえればと思います。
次に、Node.jsのLTS版(現時点ではv10かv12)をインストールします。以上で準備は完了です。
あとは、GitHubのプロジェクトをcloneして依存モジュールをインストールして起動するだけです。
git clone https://github.com/Alfresco/alfresco-content-app.git
cd alfresco-content-app
npm install
npm start

起動したら自動的にブラウザでログイン画面が開くはずですが、開かない場合は以下のURLを手動で開いてみてください。

http://localhost:4200/

ログインしてサンプルサイトの文書ライブラリを開くと以下のようにフォルダやファイルにアクセスできると思います。

Alfresco Content Applicationの画面

少し使ってみていただければ、コンテンツ操作に特化していてShareに比べてかなりシンプルなUIになっていることがわかるのではないかと思います。
コンテンツ操作に関しては、ファイル/フォルダのアップロードや検索もできますし、新規ボタンから「ライブラリの作成」でサイトを新規に作ってその文書ライブラリにアクセスすることもできます。ただ、サイトメンバーの管理はできないようなので、今のところはそれはShareからやるしかなさそうです。

Tuesday, March 31, 2020

Liferay DXP 7.1 言語リソースカスタマイズの共有

こんにちは、うです。

前回、Liferay DXP 7でのカスタイムモジュール(ポートレット)の言語リソースを他モジュールに共有する方法を紹介しました。今回は、Liferay DXP 7.1以降、既存モジュール言語リソースに対するカスタマイズを複数既存モジュールに適用する方法を紹介します。

みなさまはすでにご存知かもしれませんが、Liferay DXP 7.1から、モジュール言語リソースのカスタマイズ方式は従来のOSGI-Service方式からOSGI-Capability方式になりました。従って、bnd-tools Provide-Capabilityのみで、複数モジュールが同じ言語リソースカスタマイズを利用できます。

公式例:
Provide-Capability:\
  liferay.resource.bundle;\
    resource.bundle.base.name="content.Language",\
  liferay.resource.bundle;\
    resource.bundle.aggregate:String="(bundle.symbolic.name=com.liferay.docs.l10n.myapp.lang),\
      (bundle.symbolic.name=com.liferay.blogs.web)";\
    bundle.symbolic.name=com.liferay.blogs.web;\
    resource.bundle.base.name="content.Language";\
    service.ranking:Long="2";\
    servlet.context.name=blogs-web

複数カスタマイズ共有(例として、blogs-webproduct-navigbation-control-menu-webを対象にする)例。

Provide-Capability:\
  liferay.resource.bundle;\
    resource.bundle.base.name="content.Language",\
  liferay.resource.bundle;\
    resource.bundle.aggregate:String="(bundle.symbolic.name=com.liferay.docs.l10n.myapp.lang),\
      (bundle.symbolic.name=com.liferay.blogs.web)";\
    bundle.symbolic.name=com.liferay.blogs.web;\
    resource.bundle.base.name="content.Language";\
    service.ranking:Long="2";\
    servlet.context.name=blogs-web
  liferay.resource.bundle;\
    resource.bundle.aggregate:String="(bundle.symbolic.name=com.liferay.docs.l10n.myapp.lang),\
      (bundle.symbolic.name=com.liferay.product.navigation.control.menu.web)";\
    bundle.symbolic.name=com.liferay.product.navigation.control.menu.web;\
    resource.bundle.base.name="content.Language";\
    service.ranking:Long="2";\
    servlet.context.name=product-navigation-control-menu-web

まとめると、Liferay DXP 7.1にでの言語リソース定義は、以下のルールに従いましょう。

  •  bnd.bndProvide-Capabilityを追加する
  •  Provide-Capabilityに以下のディレクティブを追加する
    1. liferay.resource.bundle;resource.bundle.base.name=...: 言語キー所在パッケージパスを定義する
    2. liferay.resource.bundle;resource.bundle.aggregate...: カスタム対象モジュールを宣言する
    3. 複数の対象モジュールがある場合、上記2のディレクテイブを複数記入する
    4. 全てディレクテイブはコンマ,で区切る

Alfresco Community Edition 201911GAをDocker Composeでデプロイする

こんにちは。てらしたです。前回投稿したのがいつだったのか思い出せないくらい久しぶりですが、これからまた少しずつ書いていけたらと思っています。

今回は、Alfresco Community EditionをDocker Composeを使用してデプロイする方法をご紹介します。Alfrescoがどういうものなのかちょっと使って評価してみたいという場合には、とても簡単にできるのでおすすめです(ただし、本番で使う場合にはDocker Composeは推奨されていません)。
使用するバージョンは現時点(2020年3月)での最新のGA版であるAlfresco Community Edition 201911 GAです。リリースノートはこちら。AlfrescoやShareの6.2.0が含まれています。
手順については以前ニコラが紹介したこちらの内容からほとんど変わっていませんが、Alfresco Platformにもポート8080でアクセスできる点等、ちょっとした違いがあります。
また、基本的には公式ドキュメント(https://docs.alfresco.com/community/tasks/deploy-docker-compose.html)の手順どおりですが、URLにバージョンが含まれていないことからもわかるように、Community版のドキュメントはその時点の最新版のGAのものしか公開されていないようです(私が見つけられないだけかもしれませんが...)。そのため、上記の公式ドキュメントは現時点では201911GAの内容が書かれていますが、新しいバージョンがリリースされたら更新されて下記でご紹介する手順から変わってしまう可能性があります。下記の内容は201911GAのデプロイ方法である点にご注意ください。

まず準備として、手元の環境に合わせてDockerとDocker Composeをインストールしておいてください。

次に、GitHubのプロジェクトをcloneして、201911GAに対応するタグ(3.0.1)をチェックアウトします。
git clone https://github.com/Alfresco/acs-community-deployment.git
cd acs-community-deployment
git checkout 3.0.1
あとは、Docker Composeを使用して起動するだけです。この時、ポート5432、8080、8083が空いている(使われていない)ことをあらかじめ確認しておいてください。
cd docker-compose
docker-compose up
特にエラーが出ることもなく以下のようなログが表示されたら無事に起動しているはずです(起動できない場合については後述)。
alfresco_1               | 17-Mar-2020 01:56:09.395 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 97601 ms

正常に起動しているかどうかはAlfresco Shareに以下のURLでアクセスして、adminでログインできるかどうかでも確認できます(adminの初期パスワードはadminです)。

http://localhost:8080/share

また、Alfresco PlatformやSolrには以下のURLでアクセスできます。

http://localhost:8080/alfresco
http://localhost:8083/solr

正常に起動できない場合は、Dockerに割り当てているメモリが不足している可能性があります(私も一度それで失敗しました)。Dockerの設定で最低でも6GBのメモリを割り当てた上で改めて試してみてください。

Tuesday, March 17, 2020

Liferay DXP 7でツールチップとヘルプアイコンを実装する

こんにちは。おおたにです。

今回はツールチップとヘルプアイコンの実装方法について紹介します。
ツールチップと言えば、以下のようなやつですね。ちょっとした説明をポップアップ表示させられるのでなかなか便利です。


Liferayでは、ヘルプアイコンとセットになったツールチップ(上の画像のやつ)の部品が用意されているほか、任意のHTML要素へのツールチップ追加も簡単にできます。それぞれについて見ていきましょう。

ヘルプアイコン


ヘルプアイコンの表示にはliferay-ui:icon-helpというタグを使います。例えばこんな感じに書くだけで、

<p>help icon 2<liferay-ui:icon-help message="msg.tooltip.help-icon"/></p>

以下のようなヘルプアイコン&ツールチップが実装できます。messageに言語キーを指定すると多言語化もできます。超簡単!


Liferay Development Tutorialsに詳しい説明が載っていますのでご参照ください。

任意のHTML要素へのツールチップ追加


HTML要素のclass属性にlfr-portal-tooltipを追加し、data-title属性にツールチップの文言を指定するだけで、任意のHTML要素にツールチップを追加できます。例えば、

<p class="lfr-portal-tooltip" data-title="tooltip for html element">html element 1</p>

と書くだけで、以下のようなツールチップが表示されるようになります。こちらも簡単!


こちらを多言語化する場合は、JSP式(JSP expression)等を使ってdata-title属性に多言語化された文字列を指定する必要があります。

data-title="<%=LanguageUtil.get(request, "msg.tooltip.html-element") %>"

今回は以上になります。是非みなさんもツールチップを使ってみてください!

Tuesday, March 3, 2020

Liferay DXP 7でカスタムモジュールの言語リソースを共有する

こんにちは。おおたにです。 

突然ですが、Liferay DXP 7のカスタマイズを進める中で、「前に作ったモジュールの言語リソースをこちらでも使いたいな」とか「言語リソースの定義が複数のモジュールに散らばっていて管理しずらいな」と感じたことはありませんでしょうか?
今回は、上記のようなシチュエーションで役立つ、カスタムモジュールの言語リソースを共有するための方法を紹介します。

方法はとてもシンプルで、モジュールのbnd.bndに以下の内容を追記すると、他のモジュールで定義された言語リソースを参照できるというものです。
-liferay-aggregate-resource-bundles: \
    <参照したい言語リソースを持つモジュールのSymbolicName>

例えば、言語リソース一元管理用のモジュールを用意して各モジュールから参照させることで、言語リソースの管理コストや言語リソース更新し忘れによる表記揺れのリスクを低減することができます。
詳しくはこちらのドキュメントこちらのドキュメントをご参照ください。Liferay社が提供するサンプルも参考になりますので是非一度確認してみてください。