Friday, May 29, 2020

ResourcePermissionの考察 (3)

前回、Liferayのリソース権限のDBレコードについて考察しました。今回は権限チェックの実行について考察したいと思います。

Liferayは開発者が直接にResourcePermissionを利用しないように設計されています。開発者フォーラムを見ているとit is inadvisable to manipulate the Liferay database directlydo not ever change it or write to the database(Liferayデータベースを直接操作することは推奨されていません。なので、直接変更したり、データベースに書き込んだりしないでください。)
のようなコメントがしばしばあります。そのため、Liferayはユーザ権限操作機能を提供しています。では、今回は権限操作機能中に一番基本のPermissionCheckerから考察しましょう。

PermissionCheckerのメソッド

Liferayでは、ユーザごとにPermissionCheckerのインスタンスを持っています。PermissionCheckerResourceLocalServiceを介してResourcePermissionテーブルレコードを確認する形でユーザの権限をチェックしてるため、権限チェックのメソッドhasPermission()メソッドシグネーチャResourcePermissionテーブルの要素になります。
boolean hasPermission(group, name, primKey, actionId);
  • LiferayのcompanyIdgroupから取得できるため、PermissionCheckerではcompanyIdに代わりにgroupをパラメータとして利用している
  • ユーザロールはPermissionCheckerのプロパティーのためメソッドに含まれていない
まとめると、以下の情報をPermissionCheckerに提供すれば、Liferayはユーザの権限を判断することができます。
  • リソースのグループID
  • リソース名
  • リソースのPrimeKey
  • アクションID
では、リソース毎にPermissionChecker提供する情報を考察しましょう。

Liferayにおいての権限チェック

あるユーザが、あるリソースに対する操作の権限をチェックする際の手順を考察しましょう。ResourcePermissionテーブルのレコードはロール毎に記録されているため、権限チェックの際、ユーザのロールを全部取得し、PermissionCheckerにおいて以下の判断を行うことが必要です。
※ 共通パラメータactionIdを除外し、roleIdPermissionCheckerが読み出せるため除外する
リソース種別 scope 必要パラメータ チェック内容
ポートレット 1 portletId
companyId
companyIdのLiferayインスタンス中
portletIdで特定されるポートレットに対する操作権限
primKey=companyId
2 portletId
groupId
groupIdのサイト中
portletIdで特定されるポートレットに対する操作権限
primKey=groupId
3 portletId サイトロールが割り当てられる際のみ
portletIdで特定されるポートレットに対する操作権限
primKey=0
4 portletId
layoutId
primKey=layoutId + _LAYOUT_ + portletId
で特定するポートレットに対する操作権限
インスタンス化
できない仮想
モデル
1 モデル名
companyId
companyIdのLiferayインスタンス中
モデル名で特定されるモデルに対する操作権限
primKey=companyId
2 モデル名
groupId
groupIdのサイト中
モデル名で特定されるモデルに対する操作権限
primKey=groupId
3 モデル名 サイトロールが割り当てられる際のみ
モデル名で特定されるモデルに対する操作権限
primKey=0
4 - 仮想モデルはインスタンスを持っていない
インスタンス化
可能なモデル
1 モデル名
companyId
当該ロールはポータル範囲の操作権限を持つこと
primKey=companyId
2 モデル名
groupId
groupIdのサイト中
モデル名で特定されるモデルに対する操作権限
primKey=groupId
3 モデル名 サイトロールが割り当てられる際のみ
モデル名で特定されるモデルに対する操作権限
primKey=0
4 モデル名
primaryKey
primaryKeyで特定されるモデルリソースに対する操作権限
Liferayでは、companyIdgrouplayoutなどエンティティーから取得できるため、権限チェックが必要なメソッドは以下となります。

リソース種別 scope 権限チェックメソッド Liferayのインターフェース
ポートレット 1 check(permissionChecker, group, portlet, action)
3の場合、groupId=0
PortletPermission
2
3
4 check(permissionChecker, group, layout, portletId, actionId)
インスタンス化
できない仮想
モデル
1 check(permisisonChecker, group, name, action) PortletResourcePermission
nameはプロパティとして実装クラスに登録する
2
3
4 - -
インスタンス化
可能なモデル
1 check(permissionChecker, primaryKey, actionId)
check(permissionChecker, model, actionId)
ModelResourcePermission
nameはプロパティとして実装クラスに登録する
groupIdはLiferaymodelにおいてgetGroupIdで取得する
2
3
4
ここまで考察すると、ようやく第一回の課題「Liferayが提供しているPermission関連クラスのシグネーチャが異なった理由」を解決しました。

ただし、まだ解決していない課題があります。現在のLiferayにおいて、モデルリソースとして定義される仮想モデルに対する権限チェックを行うクラス名はPortletResourcePermissionです。
  • 例として、ブログポートレットのcom.liferay.blogs仮想モデルに対するADD_ENTRY権限の判定は BlogsPermission.contains(permissionChecker, scopeGroupId, ActionKeys.ADD_ENTRY)が行います。BlogsEntryPermission.javaのソースコードを確認すると、PortletResourcePermissionをOSGIサービスとして参照してることが分かります。
正直、理由が分かりません。

権限チェックに追加されたクラス

※ 以下の内容はLiferay-CE 7.1に基づいています。7.0は一部ヘルパークラスを持っていません。

Liferay7.0の権限カスタマイズに詳しい方はもう気づいたかもしれませんか、Liferay7.1からの権限設定手順で、権限を登録するに、ModelResourcePermissionLogicPortletResourcePermissionLogicなどクラスは新規追加されます。では、Liferayの標準モジュールBlogsを考察し、新規されたクラスを整理しましょう。



ブログポートレットのUIにおいて
  • ブログポートレットの設定はリソースcom_liferay_blogs_web_portlet_BlogsPortletの権限CONFIGURATIONで決まります。
  • 新しいエントリーボタンの表示可否はモデルリソースcom.liferay.blogsの権限ADD_ENTRYで決まります。
  • permission-blogの編集可否はモデルリソースcom.liferay.blogs.model.BlogsEntryの権限UPDATEで決まります。
まず、インスタンス化可能なモデルリソースcom.liferay.blogs.model.BlogsEntryの権限チェックから考察しましょう。

インスタンス化可能なモデルリソースの権限チェック

ブログエンティティの更新ボタンの表示の判定はblogs-web/src/main/resources/META-INF/resources/blogs/entry_action.jspにおいてBlogsEntryPermission.contains(permissionChecker, entry, ActionKeys.UPDATE)が行なっています。このクラスから考察すると、Liferay7.1のモデルリソースは以下のような構造のことが分かります。



ブログポートレットは、DefaultModelResourcePermissionを介して、ModelResourcePermissionLogicPermissionCheckerを用いてあるブログエンティティーに対してユーザがもっている権限をチェックしています。では、DefaultModelResourcePermissionの流れを確認しましょう。


では、フローグラフ中のModelResourcePermisisonLogic[]はどこから登録されますか?その答えはBlogsEntryModelResourcePermissionDefinition.javaにあります。
@Override
public void registerModelResourcePermissionLogics(
 ModelResourcePermission modelResourcePermission,
 Consumer>
  modelResourcePermissionLogicConsumer) {

 modelResourcePermissionLogicConsumer.accept(
  new StagedModelPermissionLogic<>(
   _stagingPermission, BlogsPortletKeys.BLOGS,
   BlogsEntry::getEntryId));
 modelResourcePermissionLogicConsumer.accept(
  new WorkflowedModelPermissionLogic<>(
   _workflowPermission, modelResourcePermission,
   _groupLocalService, BlogsEntry::getEntryId));
}
続いてStagedModelPermissionLogic.javaWorkflowedModelPermissionLogic.javaを考察しましょう(StagingについてはStaging 公式ドキュメントを参考してください)。

  • StagedModelPermissionLogicは、StagingPermissionImpl.javaを介して、サイトページ状態がStageの場合、一定操作(例:ACCESS, VIEWなど)以外を禁止する
  • WorkflowedModelPermissionLogicは、WorkflowPermissionImpl.javaを介して、ユーザの公開前コンテントに対するアクセス権限を判定する

すなわち、登録したのは本来の権限判定対象であるブログエンティティのResourcePermissionテーブルレコードに関係ないロジックです。

ここまで考察すると、Liferay 7.1の権限構造をようやく理解できます。DefaultModelResourcePermissionModelResourcePermisisonLogicPermissionChecker両方を参照する理由は、ModelResourcePermisisonLogicPermissionCheckerの役割が違うのです:
  • 開発者はModelResourcePermissionLogicを介してModelResourcePermissionに登録したLiferayのResourcePermissionテーブルに関係ないビジネスロジック
  • 登録された権限ビジネスロジックは全てnullを返す場合、PermissionCheckerを利用し、ResourcePermissionテーブルからユーザ権限をチェックします。
Liferay 7.0のBlogsEntryPermission.javaと比べてみると、7.1の権限チェッククラスの各機能は切り離されていることが分かります。

Liferay 7.0 Liferay 7.1
BlogsEntryPermissionは直接的に権限判定を行う BlogsEntryPermissionModelResourcePermission
を介して権限判定を行う
StagingPermissionUtilWorkflowPermissionUtil
を直接参照する
OSGIサービスのxxxPermissionLogic方式で
ResourcePermissionテーブルに関係ない権限ロジック
を登録する
PermissionCheckerを直接参照する PermissionCheckerは直接参照しない

ポートレットリソースの権限チェック

ブログポートレットの設定などボタンの表示はcom_liferay_blogs_web_portlet_BlogsPortletポートレットリソースの権限チェックにおいて判定しています。ポートレット権限の判定はLiferayの組み込み機能のため、公式ドキュメントの説明の通りresource-actionsにアクション定義を入れたらLiferayは自動的にブログポートレット権限チェックを行います。
ポートレットの権限判定はモデルリソースのようなOSGI構造を持っていません。具体的にはPortletPermissionImpl.javaを参考してください。

仮想モデルリソースの権限チェック

  •  BlogsPermission.contains(permissionChecker, scopeGroupId, ActionKeys.ADD_ENTRY)が行います。
前述の通り、BlogsPermission.javaPortletResourcePermissionを参照し権限チェックを行なっています。PortletResourcePermissionの構造はModelResourcePermissionとほぼ同じのため、考察を省略します。



PermissionCheckerの正体

ここまで考察すると、Liferayでは、PortletPermissionPortletResourcePermissionModelResourcePermissionなどヘルパークラスを介してPermissionCheckerインタフェースを利用し、違うリソースに対する権限チェックを行っていることが分かりました。

では、最後にPermisisonCheckerの正体を確認しましょう。ポートレットJSPでPermissionCheckerのクラス名を出力すると、StagingPermissionChecker.javaのことが確認できますが、ソースコードを見ると、StagingPermissionCheckerはもう一つ_permissionCheckerをラップしています。続いてPermissionCheckerFactoryImpl.javaを確認すると、PermissionCheckerの正体はPropsValues、すなわちportal.propertiesで設定されたクラス名です。
public PermissionCheckerFactoryImpl() throws Exception {
  Class clazz =
    (Class)Class.forName(
      PropsValues.PERMISSIONS_CHECKER);

  _permissionChecker = clazz.newInstance();
}
皆さまは多分すでに存知ですが、LiferayのシステムPermissionCheckerは、portal-ext.propertiesで設定できます。
# Set the default permission checker class used by
# com.liferay.portal.security.permission.PermissionCheckerFactory to check
# permissions for actions on objects. This class can be overriden with a
# custom class that implements
# com.liferay.portal.security.permission.PermissionChecker.
#
#permissions.checker=com.liferay.portal.security.permission.SimplePermissionChecker
permissions.checker=com.liferay.portal.security.permission.AdvancedPermissionChecker
そして、Liferayのデフォルトの状態のPermissionCheckerの正体はAdvancedPermissionChecker.javaであることが分かりました。さらに、portal-ext.propertiesにカスタマイズクラスを登録するとLiferayのデフォルトPermissionCheckerを変更することもできます。

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/

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



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