null ResourcePermissionLogicで柔軟な権限管理を行う

ResourcePermissionLogic

Liferayのエンティティ権限チェック周りの仕組みは以前から大幅に拡張され、ModelResourcePermissionとPortletResourcePermissionに分られ、エンティティ個体の操作、eg: 更新、削除など(ModelResourcePermission)とポートレット上の不特定なエンティティの操作、eg:新規作成(PortletResourcePermission)を別々で実装できます。

その実装はソースコードで確認するとRegistrar方式とPermssionWrapper方式が挙げられますが、両方とも基本的にModel/PortletResourcePermissionFactory.create()メソッドでResourcePermissionエンティティを作成しています。では、Model/PortletResourcePermissionFactory.createメソッドのシグネチャーを確認してみましょう。

ModelResourcePermissionFactory.create(
    Class<T> modelClass,
    ToLongFunction<T> primKeyToLongFunction,
    UnsafeFunction<Long,T,? extends PortalException> getModelUnsafeFunction,
    PortletResourcePermission portletResourcePermission,
    ModelResourcePermissionConfigurator<T> modelResourcePermissionConfigurator) 
PortletResourcePermissionFactory.create(
    String resourceName,
    PortletResourcePermissionLogic... portletResourcePermissionLogics) 

上記ModelResourcePermissionConfiguratorの中身もResourcePermissionLogicであることが判断できます

@FunctionalInterface
public interface ModelResourcePermissionConfigurator
        <T extends GroupedModel> {

  public void configureModelResourcePermissionLogics(
    ModelResourcePermission<T> modelResourcePermission,
      Consumer<ModelResourcePermissionLogic<T>> consumer);
}

では、この両方に存在するResourcePermissionLogicはなんでしょうか。

public interface ModelResourcePermissionLogic<T extends GroupedModel> {
    public Boolean contains(
            PermissionChecker permissionChecker, String name, T model,
            String actionId)
        throws PortalException;

}
public interface PortletResourcePermissionLogic {
    public Boolean contains(
        PermissionChecker permissionChecker, String name, Group group,
        String actionId);
}

Liferayの公式ドキュメントに以下の説明があります。

ModelResourcePermissionLogic classes return true when users have permission for the action, false when they are denied permission for the action, and null when wanting to delegate responsibility to the next permission logic. If all permission logics return null then the PermissionChecker.hasPermission method is called to determine if the action is allowed for the user.

そう、Model/PortletResourcePermissionLogicはLiferay本来のRBACのPermissionCheckerの前に存在するビジネスロジックのチェーンであります。ResourcePermissionLogicのメソッドはnullが返ると次のResourcePermissionLogicにチェーンし、true/falseが返る場合はRBACの権限設定(すなわちResourcePermissionテーブルのレコード)を上書きできます(危ない!)。

例えばLiferayのデフォルトModelResourcePermissionLogicの一つStagedModelPermissionLogicでは、サイト(グループ)はStaged状態になる場合、モデルに対する操作は、指定のものを除いて全て禁止されます。たとえロールに権限がある場合でも。

なので、Liferayの拡張された権限管理のロジック流れはこうなると想定できますね。

┌──────────────────────────────────────────────────────────────────────────────────────────┐
│<EntityModel/Portlet>ResourcePermission                                                   │
│ ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐            ┌──────────┐ │
│ │                  │  │                  │  │                  │            │          │ │
│ │ <Model/Portlet>  │  │ <Model/Portlet>  │  │ <Model/Portlet>  │    ...     │Permission│ │
│ │ PermissionLogic1 ├──▶ PermissionLogic2 ├──▶ PermissionLogic3 │───▶   ────▶│ Checker  │ │
│ │                  │  │                  │  │                  │            │          │ │
│ └──────────────────┘  └──────────────────┘  └──────────────────┘            └──────────┘ │
└──────────────────────────────────────────────────────────────────────────────────────────┘

 

柔軟な権限管理

PermissionLogicチェーンはDBでのRBACチェックの前に権限をカスタマイズできるため、より柔軟な管理ができます。今回はLiferay開発に馴染みの公式トレーニングサンプルGradebookにResourcePermissionLogicを実装してみましょう。Gradebookには、先生(Teacher)ロールは宿題Assignmentを作成、修正または削除をできるように実装されています。Wrapperで実装すると以下になります。

@Component(

    property = "model.class.name=com.liferay.training.gradebook.model.Assignment",
    service = ModelResourcePermission.class
)
public class AssignmentPermissionWrapper
    extends BaseModelResourcePermissionWrapper<Assignment> {     

    @Override
    protected ModelResourcePermission<Assignment> doGetModelResourcePermission() {         
      return ModelResourcePermissionFactory.create(
            Assignment.class, Assignment::getAssignmentId, _assignmentLocalService::getAssignment, 
            _portletResourcePermission,
            (modelResourcePermission, consumer) -> { });
    }     
    @Reference
    private AssignmentLocalService _assignmentLocalService;     
    @Reference
    private GroupLocalService _groupLocalService;     
    @Reference(target = "(resource.name=com.liferay.training.gradebook)")
    private PortletResourcePermission _portletResourcePermission;
}

ModelResourcePermissionFactory.createの最後の引数ModelResourcePermissionConfiguratorは空か、またはLiferayのソースコードのようにデフォルトのStagedModelPermissionLogic/WorkflowedModelPermissionLogicが入っている状態であれば、Assignmentの権限チェックは基本的にチェーンの最後のPermissionCheckerに到着します。先生(Teacher)ロールにVIEW、UPDATEとDELETEをアサインすると、画面上、liferay-ui:search-containerを利用してAssignmentを表示すると、先生は宿題の閲覧、編集と削除ができます。

続いて、以下のロジックを実装してみましょう:

宿題のタイトルの中に「重要」を表す単語が存在する場合、「先生」ロールのあるユーザでも当該宿題の編集と削除ができません

まずはImportantAssignmentPermissionLogicを作成しましょう。当該ロジックは、宿題のタイトルに「important」単語のある場合のみ「false=権限なし」を戻し、その他の場合は「null=次のLogicまたはPermissionCheckerに任せる」を戻します。

public class ImportantAssignmentPermissionLogic<T extends GroupedModel>
    implements ModelResourcePermissionLogic<T> {

    @Override
    public Boolean contains(PermissionChecker permissionChecker, String name, T model, String actionId)
            throws PortalException {

        Assignment asgn = (Assignment)model;
        
        if (asgn.getTitle().contains("important")) {
            System.out.println("Assignment: " + asgn.getAssignmentId() 
                + ", title = " + asgn.getTitle() 
                + " is important, you can not do anything on it");
            return false;
        }
        
        return null;
    }
}

そしてImportantAssignmentPermissionLogicを先述AssignmentPermissionWrapperのModelResourcePermissionFactory.createの最後の引数に渡します。

@Override
protected ModelResourcePermission<Assignment> doGetModelResourcePermission() {
    return ModelResourcePermissionFactory.create(
        Assignment.class, Assignment::getAssignmentId, _assignmentEntryLocalService::getAssignment, 
        _portletResourcePermission,
        (modelResourcePermission, consumer) -> {
            consumer.accept(new ImportantAssignmentPermissionLogic<>());
        });
}

こうすると、たとえ「先生」ロールを持つユーザがログインし宿題リストを開いても、先頭の「important xxxx」のタイトルを持っている宿題に更新/削除の権限はなくなります。ただし、その他の宿題への権限はロール定義に従って持っています。

log:

Assignment: 32574, title = important en-20119-dfdfdfdf is important, you can not do anything on it,

こうなると、AssignmentエンティティのResourcePermissionテーブルのレコードを更新しなくても、カスタマイズ権限ロジックを実装できました。

その他、Model/PortletResourcePermissionLogicのcontainsメソッドの引数にPermissionCheckerがあるため、そのメソッドの中にPermissionCheckerに格納された当該スレッドのユーザ情報にアクセスでき、ユーザ特化の権限カスタマイズもできます。例えば

if (permissionChecker.getUser()
  .getExpandoBridge().getAttribute("vip", false).equals(Boolean.TRUE))
  return true;
else
  return false;

にすると、「ユーザのカスタマイズフィールドである"VIP"の値がTrueに限り」"important"キーワードのある宿題を削除できます。同じく、ResourcePermissionテーブルのレコードには変更ありません。

制限と注意点

ひとまず、上記のカスタマイズは管理者の権限も影響するのため、ResourcePermissionLogicの実装内で管理者ロールを確認する必要があります。

Model/PortletResourcePermissionLogic.contains()メソッドの引数に「actionId」があり、特定アクションに対する権限チェックのカスタマイズができますが、VIEWは例外です。その理由は、確かにResourcePermission周りの実装でVIEW権限をカスタマイズできるが、LiferayのServiceBuilderが作成した「VIEW権限のあるエンティティのみを取得する」メソッド「filterByXXXX/filterCountXXXX」シリーズは、上記ResourcePermissionLogicsのカスタマイズを完全にバイパスし、直接DBのResourcePermissionテーブルからVIEW権限のあるレコード(テーブルにはResourcePermission.viewActionId = 1と表示される)を取得するので。

なので、もしVIEW権限もカスタマイズすると、ServiceBuilderのfilterBy/CountメソッドはVIEW権限のないエンティティも戻す可能性があります。このケースを避けたいので、VIEWに関するカスタマイズをしないほうが良さそうです。

まとめると、ResourcePermissionLogicsは、権限周りの細かいビジネスロジックの制御をエンティティとテーブルデータから解放し、独立したクラスに移動できます。同時に、DBの操作も少なくなります。ただし、自由にユーザ情報と権限操作するリスクは高いので、ちゃんと設計した権限ロジックのある場合だけ有効なカスタマイズツールとして利用するのがよいと思います。

RANKING
2021.1.08
2020.12.28
2020.12.01
2020.10.30
2020.12.18