intra-mart Accel Platform 認可 拡張プログラミングガイド 第5版 2014-04-01

サブジェクト拡張ガイド

サブジェクト拡張とは

intra-mart Accel Platform では標準的に以下のようなサブジェクトを提供しています。

  • 認証状態
  • ロール
  • ユーザ
  • 組織、役職
  • パブリックグループ、役割

これによって特定の組織に所属するユーザに対して権限を与えるといったことが可能です。

上記以外の解釈でユーザを分類して権限管理をしたい場合にはサブジェクトの拡張機能を作成することで、認可機構に追加して利用できます。

ここではサブジェクト拡張を作成するための手順を解説します。

おおまかな流れ

以下の流れで手順を説明します。

  1. サブジェクトの定義 を行います。

    サブジェクトの特徴を表現するためにサブジェクトの定義を行います。すでにあるマスタデータなどを連携したい場合は単純にそのデータを表すもの(キー情報など)で良いでしょう。そうでない場合、サブジェクトを表すのに必要な要素を定義します。

    この時に、ユーザに対して自明なサブジェクトか、そうでないかを明確にしておきます。

  2. 認可機構へのプラグイン実装を追加します。

    最大で以下の4種の実装が必要です。

    ../../_images/SubjectExtAll.png
    • SubjectType ( 図中 1 )

      サブジェクトのモデルと認可のサブジェクト情報の相互変換、シリアライズを担当します。

    • SubjectResolver ( 図中 2-a および 2-b )

      ユーザが何者であるかを判断する処理です。これも新たに追加するサブジェクトモデルの情報を取り扱うための実装を追加します。

      取り扱う情報の特性によって2種の実装の内どちらか ( 図中 2-a および 2-b ) を選択します。

    • 認可サブジェクトコンテキストのデコレータ ( 図中 3 )

      認可サブジェクトコンテキストはユーザが何者であるかをコンテキストとして保持していますが、このコンテキストに新たに追加するサブジェクトモデルの情報を取り扱うためのデコレータを追加します。

      SubjectResolver の実装で 2-b を選択している場合はこれは必要ありません。

    • 検索画面 ( 図中 4 )

      認可設定画面でユーザがサブジェクトとして選択できるようにするための画面拡張を追加します。

    追加しようとしているのがサブジェクトがユーザに対して自明なサブジェクトではない場合、認可サブジェクトコンテキストのデコレータは必要ありません。

  3. ポリシー部分編集設定 を編集してリソースグループセット(特定のルート配下のリソースツリー)毎に利用するサブジェクトタイプを設定します。

    サブジェクトタイプは追加しても認可設定画面で使えるようにはなりません。サブジェクトタイプをどのリソース群に対して有効にするかの設定を変更する必要があります。

以降、順に説明します。

サブジェクトの定義

サブジェクトの特徴を表現するためにサブジェクトの定義を行います。今回例として扱うサンプルアプリケーションでは以下の2種類の権限の扱いを考えています。

  1. ユーザの勤続の長さによって権限を変える
  2. データの作成者か否かによって権限を変える

前者はIM共通マスタのユーザプロファイルを見て、プロファイルが有効になってから当日までの期間の長さによって異なるサブジェクトとして解釈することにします。このサブジェクトを追加することによって認可設定画面で「プロファイルがXXカ月以上有効」といったサブジェクトを設定できる事を目標とします。

後者はサンプルアプリケーションにおいて、データを登録する際にデータベースのレコードに作成者を記録しておき、ログインユーザが作成者と一致する場合そのデータの「オーナー」というサブジェクトとして解釈することとします。 これによって認可設定画面で「リソースオーナー」といったサブジェクトを設定できる事を目標とします。

ここで、ユーザに対して自明なサブジェクトになるかどうかを明確にしておく必要があります。

認可のサブジェクト解決のタイミングは2段階ありユーザのログイン時(厳密にはコンテキストの作成/切替のタイミング)と、認可判断時です。前者はログイン時に明確になっているサブジェクト情報(所属組織など)を解決し、コンテキストに保持します。後者はログイン時にはわからない、状況依存のようなサブジェクトを解決します。

「プロファイルがXXカ月以上有効」かどうかはユーザが誰であるかが判明すれば、ユーザに付随する情報から取得できる情報です。つまりログイン時に判断できます。 これに対して「リソースオーナー」はユーザが誰であるかだけでなく、どのデータについて判断しようとしているかが分からないと判断できません。

つまり

  • 「プロファイルがXXカ月以上有効」 : ユーザに対して自明。ログイン時に判断可能。
  • 「リソースオーナー」 : ユーザに対して自明ではない。認可要求時でなければ判断できない。

という事です。

SubjectType

SubjectTypeとは

アクセス主体の定義実体と認可のサブジェクトの相互変換、およびシリアライズを担当します。

認可機構からサブジェクトに対して付与されるSubjectIDとアクセス主体の定義実体のキー情報とのマッピングを管理する責務があります。認可設定画面で新たなサブジェクトを選択して権限を設定しようとする場合、選択したサブジェクトにSubjectIDが割り当てられます。この時、SubjectTypeは実際に選択されたモデルとSubjectIDの対応を管理しなければなりません。

サブジェクトタイプは認可の持つ「AND」「OR」「NOT」といった複合の条件を考慮する必要はありません。この条件は認可機構側で考慮します。ただし、連携マスタ側でなければ判断できない条件はSubjectType側でサポートしなければなりません。例えば、IM共通マスタの組織情報などにおいて、下位組織といった条件は認可機構側では判断できないため、SubjectTypeがなんらかの情報で判断できるように情報を保持するようにします。

SubjectTypeで使用するモデルクラスの作成

まずサブジェクトタイプで使用するモデルクラスを作成します。既存のクラスで問題なければ使いまわしても構いません。例えば、デフォルトでインストールされるIM共通マスタのユーザサブジェクトの場合、IM共通マスタのAPIが提供しているjp.co.intra_mart.foundation.master.user.model.User というモデルクラスをそのまま使用しています。同じくIM共通マスタの組織サブジェクトの場合、組織モデルの情報に加えて上位組織や下位組織を含めるか否かの情報も含めてサブジェクトタイプが管理する必要があるため、IM共通マスタのAPIが提供しているクラスと上位下位の条件を合わせて持つモデルクラスを新たに定義しています。

サンプルでは以下の2種類を作成しています。

  • 「プロファイルがXXカ月以上有効」であるユーザを表すモデルクラス
  • 「リソースオーナー」であることを示すモデルクラス

まず「プロファイルがXXカ月以上有効」なユーザのモデルクラスのソースコードを以下に示します。これは有効な月数だけがわかればよいので、intフィールドを一つを定義したクラスを作成します。

/**
 * ProfileKeepAlive クラス
 * @author INTRAMART
 * @version 8.0
 */
public class ProfileKeepAlive {

    int months;

    /**
     * months の Getter
     * @return monthsを返却
     */
    public int getMonths() {
        return months;
    }

    /**
     * months の Setter
     * @param months monthsの設定値
     */
    public void setMonths(final int months) {
        this.months = months;
    }

}

「リソースオーナー」であることを示すモデルクラスは情報を持つ必要は特にありません。サブジェクトIDを生成するために必要なIDだけをstaticフィールドとして定義しています。

/**
 * SampleResourceOwner クラス
 * @author INTRAMART
 * @version 8.0
 */
public class SampleResourceOwner {

    // リソースオーナーであることを示すID
    public static final String IDENTIFIER = "sample-resource-owner";

}

SubjectTypeの実装

SubjectType を実装するには以下のインタフェースを実装しなければなりません。

jp.co.intra_mart.foundation.authz.model.subjects.SubjectType<T>

このインタフェースはサブジェクトのモデルと認可のサブジェクト情報の相互変換やシリアライズを行うためのメソッドが定義されています。

すでに述べている通りサンプルでは次の2種のサブジェクトタイプを定義しています。

  1. 「プロファイルが有効になってからXXカ月」といった設定ができるサブジェクトタイプ
    • ユーザのプロファイルが有効になってからの期間の長さでサブジェクトを変えるサブジェクトタイプです
    • サブジェクトタイプIDは sample-prof-keep-alive
    • この情報はログイン時に判定するものとします。(厳密な時刻までは判定しない)
  2. サンプルアプリケーションのデータの作成者を「リソースオーナー」として解釈するサブジェクトタイプ
    • あるデータにアクセスしようとした際に、そのデータの登録ユーザを参照して、ログインユーザと同一であればログインユーザをリソースオーナーというサブジェクトとして解釈します。
    • サブジェクトタイプIDは sample-resource-owner
    • この情報はリソースにアクセスする時点にならなければわかりません。

以下は sample-prof-keep-alive の実装クラスです。上記の SubjectType インタフェースを実装しており、モデルクラスとして ProfileKeepAlive を使用しています。

完全修飾クラス名
sample.subject.type.ProfileKeepAliveSubjectType
/**
 * プロファイルの有効な期間が何か月かを表すサブジェクトタイプです。
 * @author INTRAMART
 * @version 8.0
 */
public class ProfileKeepAliveSubjectType implements SubjectType<ProfileKeepAlive> {

    private static final long serialVersionUID = -3646347367576852310L;

    public static final String LEADER = "months_for_"; //$NON-NLS-1$

    @Override
    public String createIdentifier(final Object... keys) {

        // keyとして受付可能なのはinteger または数値表現の文字 1つとします。
        // そのままキーにしても問題ありませんが、保険としてプリフィックスをつけます。

        if (keys != null && keys.length == 1) {

            if (keys[0] instanceof Integer) {
                return LEADER.concat(String.valueOf(keys[0]));
            }

            if (keys[0] instanceof String && ((String) keys[0]).matches("[0-9]+")) {
                return LEADER.concat(String.valueOf(Integer.parseInt((String) keys[0])));
            }

        }

        throw new SubjectTypeRuntimeException();

    }

    @Override
    public String createIdentifier(final ProfileKeepAlive model) {

        // サブジェクトモデルをハッシュキーを生成するためのIDに変換します。
        if (model != null) {
            return LEADER.concat(String.valueOf(model.getMonths()));
        }

        throw new SubjectTypeRuntimeException();

    }

    @Override
    public I18nValue<String> getDisplayName() {

        // このサブジェクトタイプの名称です。
        final I18nValue<String> name = new I18nValue<String>();
        name.put(Locale.JAPANESE, "プロファイルの有効期間"); //$NON-NLS-1$
        return name;
    }

    @Override
    public String getSubjectTypeId() {
        // サブジェクトタイプのIDです。
        return "sample-prof-keep-alive";
    }

    @Override
    public void onCreateSubject(final Subject subject, final Object... keys) throws SubjectManagingException {

        // このメソッドは認可機構でサブジェクトを作成した際に呼び出されます。
        // データベースへ保管しておき、以後のマッピング時に参照できるようにしておきます
        final ProfileKeepAliveSID sid = new ProfileKeepAliveSID();
        sid.keepAlive = keys[0] instanceof Integer ? (Integer) keys[0] : Integer.parseInt(String.valueOf(keys[0]));
        sid.subjectId = subject.getSubjectId();

        final ProfileKeepAliveSIDDAO dao = DAOFactory.getTenantDatabaseDAO(ProfileKeepAliveSIDDAO.class);
        dao.insert(sid);

    }

    @Override
    public void onCreateSubject(final Subject subject, final ProfileKeepAlive model) throws SubjectManagingException {

        // このメソッドは認可機構でサブジェクトを作成した際に呼び出されます。
        // データベースへ保管しておき、以後のマッピング時に参照できるようにしておきます
        final ProfileKeepAliveSID sid = new ProfileKeepAliveSID();
        sid.keepAlive = model.getMonths();
        sid.subjectId = subject.getSubjectId();

        final ProfileKeepAliveSIDDAO dao = DAOFactory.getTenantDatabaseDAO(ProfileKeepAliveSIDDAO.class);
        dao.insert(sid);

    }

    @Override
    public void onRemoveSubject(final Subject subject) throws SubjectManagingException {

        // このメソッドは認可機構でサブジェクトを削除した際に呼び出されます。
        // 認可機構から削除されたので、テーブルからも削除します
        final ProfileKeepAliveSID sid = new ProfileKeepAliveSID();
        sid.subjectId = subject.getSubjectId();

        final ProfileKeepAliveSIDDAO dao = DAOFactory.getTenantDatabaseDAO(ProfileKeepAliveSIDDAO.class);
        dao.delete(sid);

    }

    @Override
    public List<Object> parseIdentifier(final String identifier) {

        // createIdentifier で作成する文字列のパース処理です。
        // キーのリストを返します。

        if (identifier == null || !identifier.startsWith(LEADER)) {
            throw new SubjectTypeRuntimeException();
        }

        final List<Object> keys = new ArrayList<Object>();

        // createIdentifier と逆の処理をします。
        keys.add(Integer.parseInt(identifier.substring(LEADER.length())));

        return keys;
    }

    @Override
    public I18nValue<String> resolveDisplayName(final Object... keys) {
        final I18nValue<String> name = new I18nValue<String>();
        name.put(Locale.JAPANESE, ((String) keys[0]).concat(" カ月以上有効なユーザ"));
        return name;
    }

    @Override
    public I18nValue<String> resolveDisplayName(final String subjectId) {

        // サブジェクトIDから表示名を取得して返します。
        final ProfileKeepAliveSIDDAO dao = DAOFactory.getTenantDatabaseDAO(ProfileKeepAliveSIDDAO.class);
        final ProfileKeepAliveSID sid = dao.find(subjectId);

        if (sid == null) {
            return null;
        }

        final I18nValue<String> name = new I18nValue<String>();
        name.put(Locale.JAPANESE, String.valueOf(sid.keepAlive).concat(" カ月以上有効なユーザ"));
        return name;
    }

    @Override
    public List<Object> resolveKeys(final String subjectId) {

        // サブジェクトIDからキーのリストを取得して返します。
        final ProfileKeepAliveSIDDAO dao = DAOFactory.getTenantDatabaseDAO(ProfileKeepAliveSIDDAO.class);
        final ProfileKeepAliveSID sid = dao.find(subjectId);

        if (sid == null) {
            return null;
        }

        final List<Object> result = new ArrayList<Object>();
        result.add(sid.keepAlive);
        return result;
    }

    @Override
    public ProfileKeepAlive resolveModel(final String subjectId) {

        // サブジェクトIDからサブジェクトモデルを作成して返します。
        final ProfileKeepAliveSIDDAO dao = DAOFactory.getTenantDatabaseDAO(ProfileKeepAliveSIDDAO.class);
        final ProfileKeepAliveSID sid = dao.find(subjectId);

        if (sid == null) {
            return null;
        }

        final ProfileKeepAlive model = new ProfileKeepAlive();
        model.setMonths(sid.keepAlive);
        return model;

    }

}

sample-resource-owner の実装につきましてはここでは割愛します。必要であれば下記のクラスのソースコードを参照してください。

完全修飾クラス名
sample.subject.type.ProfileKeepAliveSubjectType

SubjectTypeの設定

SubjectTypeのクラスを実装したら設定ファイルを記述して認可機構で使えるようにします。 設定ファイルの配置場所は次の場所です。

場所
%CONTEXT_PATH%/WEB-INF/conf/authz-subject-type-config/<任意の名称>.xml
XMLスキーマの場所
%CONTEXT_PATH%/WEB-INF/schema/authz-subject-type-config.xsd

この設定ファイルの詳細については「認可仕様書」、または「設定ファイルリファレンス」を参照してください。

実装したサブジェクトタイプの実装クラスと、モデルクラスのセットを直接指定します。

<?xml version="1.0" encoding="UTF-8"?>
<p:authz-subject-type-config
    xmlns:p="http://www.intra-mart.jp/authz/authz-subject-type-config/"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.intra-mart.jp/authz/authz-subject-type-config/ ../../schema/authz-subject-type-config.xsd">

  <!-- サブジェクトタイプ sample-prof-keep-alive の設定 -->
  <subject-type
      type-class="sample.subject.type.ProfileKeepAliveSubjectType"
      model-class="sample.subject.type.ProfileKeepAlive" />

  <!-- サブジェクトタイプ sample-resource-owner の設定 -->
  <subject-type
      type-class="sample.subject.type.SampleResourceOwnerSubjectType"
      model-class="sample.subject.type.SampleResourceOwner" />

</p:authz-subject-type-config>

ChangeableNameSubjectTypeとは

参照する基準日によってサブジェクトの名称が変わることがある場合は、SubjectType インタフェースを内包する ChangeableNameSubjectType インタフェースを実装します。
例えば、IM共通マスタのユーザサブジェクトの場合、ユーザ情報は期間情報を複数持ち、基準日によって名称を変えることができますので、ユーザサブジェクトのサブジェクトタイプでは、ChangeableNameSubjectType インタフェースを実装しています。
ChangeableNameSubjectType インタフェースには、基準日を指定して名称を解決するための resolveDisplayNameWithDate メソッドがあります。
このメソッドを追加実装することで、検索基準日に応じた名称を認可設定画面の対象者条件一覧に表示できます。
SubjectType インタフェースを実装したサブジェクトタイプの場合は、検索基準日を変更しても名称の表示には影響しません。

コラム

ChangeableNameSubjectType インタフェースは intra-mart Accel Platform 2013 Winter から提供しています。

@Override
public I18nValue<String> resolveDisplayName(final Object... keys) {

    // 基準日が指定されなかった場合はテナントのタイムゾーンで現在日時を指定します。
    final TimeZone timeZone = getTenantTimeZone();
    final DateTime baseDate = new DateTime(timeZone);
    return resolveDisplayNameWithDate(baseDate.getDate(), keys);
}

@Override
public I18nValue<String> resolveDisplayName(final String subjectId) {

    // 基準日が指定されなかった場合はテナントのタイムゾーンで現在日時を指定します。
    final TimeZone timeZone = getTenantTimeZone();
    final DateTime baseDate = new DateTime(timeZone);
    return resolveDisplayNameWithDate(baseDate.getDate(), subjectId);
}

@Override
public I18nValue<String> resolveDisplayNameWithDate(Date baseDate, final Object... keys) {
    Calendar calendar = GregorianCalendar.getInstance();
    calendar.setTime(baseDate);
    int year = calendar.get(Calendar.YEAR);

    final I18nValue<String> name = new I18nValue<String>();
    if (year >= 2015) {
        // 基準日が 2015 年以上の場合は名称を変更
        name.put(Locale.JAPANESE, ((String) keys[0]).concat(" カ月以上登録されているユーザ"));
    } else {
        name.put(Locale.JAPANESE, ((String) keys[0]).concat(" カ月以上有効なユーザ"));
    }
    return name;
}

@Override
public I18nValue<String> resolveDisplayNameWithDate(Date baseDate, final String subjectId) {

    // サブジェクトIDから表示名を取得して返します。
    final ProfileKeepAliveSIDDAO dao = DAOFactory.getTenantDatabaseDAO(ProfileKeepAliveSIDDAO.class);
    final ProfileKeepAliveSID sid = dao.find(subjectId);

    if (sid == null) {
        return null;
    }

    Calendar calendar = GregorianCalendar.getInstance();
    calendar.setTime(baseDate);
    int year = calendar.get(Calendar.YEAR);

    final I18nValue<String> name = new I18nValue<String>();
    if (year >= 2015) {
        // 基準日が 2015 年以上の場合は名称を変更
        name.put(Locale.JAPANESE, String.valueOf(sid.keepAlive).concat(" カ月以上登録されているユーザ"));
    } else {
        name.put(Locale.JAPANESE, String.valueOf(sid.keepAlive).concat(" カ月以上有効なユーザ"));
    }
    return name;
}

private TimeZone getTenantTimeZone() {
    try {
        final TenantInfoManager manager = new TenantInfoManager();
        final TenantInfo tenant = manager.getTenantInfo(true);
        if (tenant == null) {
            throw new RuntimeException();
        }
        return TimeZone.getTimeZone(tenant.getTimeZoneId());
    } catch (final AdminException e) {
       throw new RuntimeException(e);
    }
}

SubjectResolver

SubjectResolver はユーザがどのサブジェクトに当たるかという事を判断するためのインタフェースです。SubjectResolver には DeclaredSubjectResolver と OndemandSubjectResolver の2種類のインタフェースがあります。

DeclaredSubjectResolver と OndemandSubjectResolver

サブジェクトリゾルバには2種類のインタフェースが提供されています。追加したいサブジェクトの特性に応じてどちらか選択する必要があります。

DeclaredSubjectResolver

完全修飾クラス名は以下です。
jp.co.intra_mart.foundation.authz.client.DeclaredSubjectResolver

ユーザが特定できた時点(例えばログイン時)でサブジェクトが決まる場合 DeclaredSubjectResolver を実装します。つまりサブジェクトにしたい情報が単純にユーザコードがわかれば割り出せる情報であることが必要です。

intra-mart Accel Platform の提供している例で言えばユーザの所属組織がこれにあたります。サンプルアプリケーションのサブジェクトタイプでは sample-prof-keep-alive が該当します。

OndemandSubjectResolver

完全修飾クラス名は以下です。
jp.co.intra_mart.foundation.authz.client.OnDemandSubjectResolver

認可要求のタイミングにならなければ決定できない情報をサブジェクトにしたい場合 OndemandSubjectResolver を実装します。認可要求時に、認可対象のリソースによってサブジェクトの解釈を変更する場合がこれに当たります。

intra-mart Accel Platform が標準で提供している中にはこの実装はありません。サンプルアプリケーションのサブジェクトタイプでは sample-resource-owner が該当します。

SubjectResolver の実行タイミング

DeclaredSubjectResolver と OndemandSubjectResolver について紹介しましたが、ここでこれらの実装が呼び出される状況について説明します。

../../_images/ContextAccessFlow.png

上図は認可判断を行うに当たってサブジェクトの解決を行うタイミングを示したシーケンス図です。認可機構ではユーザが特定できた時点(例えばログイン時)で決まるサブジェクトの場合、ログインと同時にサブジェクトを解決しておき認可サブジェクトコンテキストに保持しておきます( 図中 1 )。認可要求時には毎回サブジェクト解決を行うのではなく可能であればこの認可サブジェクトコンテキストの情報を使用して認可判断処理を行うことで、処理負荷を軽減しています。

ログイン時にサブジェクトを解決して認可サブジェクトコンテキストに格納する処理は後述する 認可サブジェクトコンテキストのデコレータ が行っています。

認可要求のタイミングにならなければ決定できないサブジェクトの場合、これでは解決できないので、認可機構では認可要求を受けたタイミングでサブジェクト解決処理を行えるようにしています ( 図中 2 )。OndemandSubjectResolver はこのタイミングで呼び出されます。

通常ログインユーザに対して認可判断を行う場合はサブジェクト解決についてはこれだけで DeclaredSubjectResolver が呼び出されることはありません。

DeclaredSubjectResolverが呼び出されるのは、バッチ処理中などのユーザがログインして利用しているわけではない状況下で認可を要求された時、またはユーザがログインしている場合でも、ログインユーザ以外のユーザに対して認可判断を要求された場合、認可機構は認可サブジェクトコンテキストが利用できないと判断し、OndemandSubjectResolver と同じタイミング ( 図中 2 ) で DeclaredSubjectResolver を実行することで、認可サブジェクトコンテキストの持つ情報を補っています。

DeclaredSubjectResolverの実装

すでに述べたとおり「プロファイルが有効になってからXXカ月」は時刻までを厳密に考えなければユーザコードがわかれば割り出せる情報です。これをサブジェクトとする場合、 リゾルバ は DeclaredSubjectResolver を実装します。

以下はサブジェクトタイプ sample-prof-keep-alive のリゾルバの例です。ユーザのプロファイルが今日まで何ヶ月間有効かを判定して、それより小さい月数を設定しているサブジェクトを取得して返します。

完全修飾クラス名
sample.subject.resolver.ProfileKeepAliveSubjectResolver
/**
 * ProfileKeepAliveSubjectResolver クラス <br>
 * プロファイルの現在までの期間の月数に応じてサブジェクトを解決します。
 * @author INTRAMART
 * @version 8.0
 */
public class ProfileKeepAliveSubjectResolver implements DeclaredSubjectResolver {

    /**
     * サブジェクトの解決処理を行います。
     * @param userCd ユーザコード
     * @return サブジェクトIDの一覧
     * @throws BizApiException
     */

    public Set<String> resolveProfileSubjectKeepAlive(final String userCd) throws BizApiException {
        final Set<String> result = new HashSet<String>();

        if (ContextStatus.isAdministrator() || !ContextStatus.isAuthenticated()) {
            // 管理者やゲストユーザの場合には何もしない
            return result;
        }

        final UserManager manager = new UserManager();
        final UserBizKey bizKey = new UserBizKey();
        bizKey.setUserCd(userCd);
        final ITerm term = manager.getUserTerm(bizKey, Env.getSystemDate().getTime());

        if (term != null && !term.isDisable()) {
            // 有効であること

            // 大体の月数を計ります
            final int months = (int) ((System.currentTimeMillis() - term.getStartDate().getTime()) / (30L * 24L * 60L * 60L * 1000L));

            // ※ 本来は現在の期間の前の期間も有効であれば加算すべきですが、ここでは説明のため省略します。

            // サブジェクトIDを保管しているProfileKeepAliveSubjectTypeを参照して
            // 月数より小さい月数を設定しているサブジェクトIDを取得します
            final ProfileKeepAliveSIDDAO dao = DAOFactory.getTenantDatabaseDAO(ProfileKeepAliveSIDDAO.class);
            result.addAll(dao.getSubjectIds(months));

        }

        return result;
    }

    /**
     * サブジェクトの解決処理を行います。
     * @param userCd ユーザコード
     * @return サブジェクトIDのセット
     * @throws Exception 解決処理に問題が発生した場合
     */
    @Override
    public Set<String> resolveSubjectIds(final String userCd) throws Exception {

        return resolveProfileSubjectKeepAlive(userCd);

    }
}

上記のとおり、 DeclaredSubjectResolver は引数にユーザコードしか渡されません。これはユーザコードがわかれば割り出せる情報であることに由来しています。

DeclaredSubjectResolverの設定

DeclaredSubjectResolverを有効にするには以下の設定を追加する必要があります。

%CONTEXT_PATH%/WEB-INF/conf/declared-subject-resolvers/<任意の名称>.xml

書式の詳細については「認可仕様書」、または「設定ファイルリファレンス」を参照してください。 以下は ProfileKeepAliveSubjectResolver を有効にするための設定例です。

<?xml version="1.0" encoding="UTF-8"?>
<p:declared-subject-resolvers
    xmlns:p="http://www.intra-mart.jp/authz/declared-subject-resolvers/"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.intra-mart.jp/authz/declared-subject-resolvers/ ../../schema/declared-subject-resolvers.xsd">

  <p:class-name>sample.subject.resolver.ProfileKeepAliveSubjectResolver</p:class-name>

</p:declared-subject-resolvers>

OndemandSubjectResolverの実装

あるリソースについて、リソースの作成者をリソースオーナーとするようなサブジェクトを実現する場合、リゾルバは OndemandSubjectResolver を実装します。

以下はサブジェクトタイプ sample-resource-owner のリゾルバの例です。認可要求されたリソースのURIから対象となるデータを取り出し、その情報の作成者を調べます。作成者が認可要求元のユーザと一致する場合、リソースのオーナーとしてのサブジェクトを解決しています。

完全修飾クラス名
sample.subject.resolver.ResourceOwnerSubjectResolver
/**
 * ResourceOwnerSubjectResolver クラス リソースの登録者をオーナーとして扱います。
 * @author INTRAMART
 * @version 8.0
 */
public class ResourceOwnerSubjectResolver implements OnDemandSubjectResolver {

    private static final String prefix = "sample-data://im-authz-ext-sample/data/";

    /**
     * サブジェクトの解決処理を行います。
     * @param userCd ユーザコード
     * @param resource リソース
     * @param action アクション
     * @return サブジェクトIDのセット
     * @see jp.co.intra_mart.foundation.authz.client.OnDemandSubjectResolver#resolveSubjectIds(java.lang.String,
     *      jp.co.intra_mart.foundation.authz.model.resources.Resource, jp.co.intra_mart.foundation.authz.model.actions.Action)
     */
    @Override
    public Set<String> resolveSubjectIds(final String userCd, final Resource resource, final Action action) {
        return resolveSubjectIds(userCd, resource.getUri(), action.toString());
    }

    /**
     * サブジェクトの解決処理を行います。
     * @param userCd ユーザコード
     * @param resourceUri リソースURI
     * @param action アクション
     * @return サブジェクトIDのセット
     * @see jp.co.intra_mart.foundation.authz.client.OnDemandSubjectResolver#resolveSubjectIds(java.lang.String,
     *      java.lang.String, java.lang.String)
     */
    @Override
    public Set<String> resolveSubjectIds(final String userCd, final String resourceUri, final String action) {

        final Set<String> result = new HashSet<String>();

        if (!resourceUri.startsWith(prefix)) {
            return result;
        }

        final String id = resourceUri.substring(prefix.length());

        // 対象のリソースのレコードを取得して作成者がユーザコードと一致するか確認
        final DataDAO dataDao = DAOFactory.getTenantDatabaseDAO(DataDAO.class);

        final Data data = dataDao.find(id);

        if (data == null || !userCd.equals(data.createUserCd)) {
            // 取得できないか、一致しない場合はサブジェクトなし
            return result;
        }

        // 一致したので、サブジェクトID を取得
        final ResourceOwnerSIDDAO sidDao = DAOFactory.getTenantDatabaseDAO(ResourceOwnerSIDDAO.class);
        final String sid = sidDao.getSid(SampleResourceOwner.IDENTIFIER);

        result.add(sid);

        return result;
    }
}

OndemandSubjectResolverの設定

OndemandSubjectResolverを有効にするには以下の設定を追加する必要があります。

%CONTEXT_PATH%/WEB-INF/conf/ondemand-subject-resolvers/<任意の名称>.xml

書式の詳細については「認可仕様書」、または「設定ファイルリファレンス」を参照してください。 以下は ResourceOwnerSubjectResolver を有効にするための設定例です。

<?xml version="1.0" encoding="UTF-8"?>
<p:ondemand-subject-resolvers
    xmlns:p="http://www.intra-mart.jp/authz/ondemand-subject-resolvers/"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.intra-mart.jp/authz/ondemand-subject-resolvers/ ../../schema/ondemand-subject-resolvers.xsd">

  <p:class-name>sample.subject.resolver.ResourceOwnerSubjectResolver</p:class-name>

</p:ondemand-subject-resolvers>

認可サブジェクトコンテキストのデコレータ

認可サブジェクトコンテキストはユーザがログインした際などにその時点で決まるサブジェクトを保持するコンテキスト情報です。SubjectResolver のところで OndemandSubjectResolver の作成を選択した場合、このデコレータを作成する必要はありません。

認可サブジェクトコンテキストはサブジェクトIDの配列と、サブジェクトグループIDの配列を主たる情報として保持していますが、コンテキストデコレータはこのうち、ユーザに対するサブジェクトIDを適切に解決する必要があります。サブジェクトIDはSubjectTypeがアクセス主体の実態とマッピングするIDです。実装するインタフェースは異なりますが、実質行わなければならない処理の内容は DeclaredSubjectResolver と変わりありません。可能なら処理を共有できるよう設計すべきです。

ここでは DeclaredSubjectResolverの実装 で DeclaredSubjectResolver として実装した「プロファイルが有効になってからXXカ月」のサブジェクトの情報をコンテキストに追加するデコレータの実装です。

/**
 * ProfileKeepAliveContextDecorator クラス
 * @author INTRAMART
 * @version 8.0
 */
public class ProfileKeepAliveContextDecorator extends ContextDecoratorSupport {

    /**
     * @param context
     * @param resource
     * @return
     * @see jp.co.intra_mart.foundation.context.core.ContextDecorator#decorate(jp.co.intra_mart.foundation.context.model.Context,
     *      jp.co.intra_mart.foundation.context.core.Resource)
     */
    @Override
    public Context decorate(final Context context, final Resource resource) {

        if (context == null || !(context instanceof AuthzSubjectContextImpl)) {
            return context;
        }

        final AuthzSubjectContextImpl authzContext = (AuthzSubjectContextImpl) context;

        final AccountContext accContext = Contexts.get(AccountContext.class);

        if (accContext == null || !accContext.isAuthenticated()) {
            // 認証が済んでいなければなにもしない。
            return context;
        }

        try {
            // SubjectResolverの処理を共有します
            final ProfileKeepAliveSubjectResolver resolver = new ProfileKeepAliveSubjectResolver();
            final Set<String> subjectIds = resolver.resolveProfileSubjectKeepAlive(accContext.getUserCd());

            for (final String sid : subjectIds) {
                // SubjectIdContainer に変換して、コンテキストに追加します
                authzContext.addSubjectIdContainer(new ProfileKeepAliveSubjectIdContainer(sid));
            }

            return authzContext;

        } catch (final BizApiException e) {
            throw new LifecycleException(e);
        }
    }

}

デコレータの実装を有効にするにはコンテキストの設定も追加する必要があります。

場所
%CONTEXT_PATH%/WEB-INF/conf/context-config/<任意の名称>.xml
XML スキーマの場所
%CONTEXT_PATH%/WEB-INF/schema/context-config.xsd

上記の実装を有効にするサンプルを以下に示します。

<?xml version="1.0"?>
<context-config
    xmlns="http://intra-mart.co.jp/foundation/context/context-config"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://intra-mart.co.jp/foundation/context/context-config ../../schema/context-config.xsd">

  <!-- 認可サブジェクトコンテキスト -->
  <context name="jp.co.intra_mart.foundation.authz.context.AuthzSubjectContext"
      depends="jp.co.intra_mart.foundation.user_context.model.UserContext">

    <!-- 認可サブジェクトコンテキスト(Webアクセス) -->
    <!-- Target for begin() -->
    <!-- Merge Decorator Settings -->
    <builder target="platform.request" >
      <builder-class>jp.co.intra_mart.system.authz.context.impl.StandardAuthzSubjectContextBuilder</builder-class>
      <decorator>
        <decorator-class>sample.subject.decorator.ProfileKeepAliveContextDecorator</decorator-class>
      </decorator>
    </builder>

    <!-- 認可サブジェクトコンテキスト(スイッチデフォルト) -->
    <!-- Target for switchTo() -->
    <!-- Merge Decorator Settings -->
    <builder target="platform.switch.default platform.request.switch.default" >
      <builder-class>jp.co.intra_mart.system.authz.context.impl.StandardAuthzSubjectContextBuilder</builder-class>
      <decorator>
        <decorator-class>sample.subject.decorator.ProfileKeepAliveContextDecorator</decorator-class>
      </decorator>
    </builder>

    <!-- 認可サブジェクトコンテキスト(スタックデフォルト) -->
    <!-- Target for stack() -->
    <!-- Merge Decorator Settings -->
    <builder target="platform.stack.default" >
      <builder-class>jp.co.intra_mart.system.authz.context.impl.StandardAuthzSubjectContextBuilder</builder-class>
      <decorator>
        <decorator-class>sample.subject.decorator.ProfileKeepAliveContextDecorator</decorator-class>
      </decorator>
    </builder>
  </context>
</context-config>

検索画面

認可設定画面で、サブジェクトとして追加できるようにするためには、サブジェクトを検索する画面が必要です。この画面は認可機構のプラグインとして実装します。

新しいサブジェクトタイプのための検索画面を追加するために plugin.xmlを用意します。

場所
%CONTEXT_PATH%/WEB-INF/plugin/jp.co.intra_mart.sample.authz.subject.search/plugin.xml
<?xml version="1.0" encoding="UTF-8"?>
<plugin>
    <extension
        point="jp.co.intra_mart.authz.subject.search" >
        <subjectSearchConfig
            name="IM-Master Authz Subject Search Config"
            id="jp.co.intra_mart.sample.authz.subject.search"
            version="8.0.0"
            rank="10"
            enable="true">

          <headerPage>sample/im_authz_ext_sample/header</headerPage>
          <buttons>
            <button
                subjectType="sample-resource-owner"
                displayName="リソースオーナー"
                action="addResourceOwner"
                >
              <options>
              </options>
            </button>
            <button
                subjectType="sample-prof-keep-alive"
                displayName="Keep Alive"
                action="addKeepAlive"
                >
              <viewPage>sample/im_authz_ext_sample/keep_alive_button</viewPage>
              <options>
              </options>
            </button>
          </buttons>

          <conditions>
            <condition id="sample.condition">
              <comparator type="ge"
                  displayName="%condition.greater_or_equal" />
            </condition>

          </conditions>

        </subjectSearchConfig>
    </extension>
</plugin>

この設定は認可の「条件の新規作成」や「条件の編集」などの操作で使用する「対象者の条件設定画面」に表示されるボタンを追加します。プラグインの設定ファイルには複数のサブジェクトタイプについて同時に設定でき、上記では sample-prof-keep-alivesample-resource-owner のサブジェクトタイプ用のボタンを設定しています。

最初に現れる <headerPage> は認可設定画面でヘッダとして読み込むページを指定します。「対象者の条件設定画面」に追加したボタンのイベントハンドラなどをここに記述します。

ここでは次のようなページを読み込んでいます。

<script type="text/javascript">

  function addKeepAlive(plugin, baseDate){

    var val = $("#sample-prof-keep-alive-value").val();

    if(!/[0-9]+/.test(val)){
      alert("数字で入力してください。");
      return;
    }

    SubjectControl.update({
        subjectType : "sample-prof-keep-alive" ,
        key         : val ,
        name        : val + " カ月以上有効なユーザ "
    });

  }

  function addResourceOwner(plugin, baseDate){

    SubjectControl.update({
        subjectType : "sample-resource-owner" ,
        key         : "sample-resource-owner" ,
        name        : "リソースオーナー(サンプル)"
    });

  }

</script>

SubjectControl.update() は対象者の一覧にサブジェクトを追加するためのメソッドです。このメソッドに対しサンプルのようにサブジェクトタイプ、キーとなる情報、名称を渡すことで対象者に追加可能です。

組織のサブジェクトなどの場合、このイベントハンドラで検索画面をポップアップし、その戻り値を受けて対象者へ追加しています。ここでは簡単にするためそこまでは行わず、ボタンをクリックしたタイミングで対象者にサブジェクトを追加しています。

サブジェクトタイプ sample-resource-owner 用のボタンは普通のボタンとして表示されます。<button> タグの action 属性に指定されているのはボタンがクリックされたときに呼び出す関数の名前です。上記の <headerPage> で指定したページなどに関数を用意しておき、ここに指定します。

この関数に渡される引数は以下の通りです。

引数名 説明 導入バージョン
plugin Object
プラグイン情報が格納されています。
plugin.subjectType: サブジェクトタイプID
plugin.displayName: ボタン表示名
plugin.action: アクション名
2012 Autumn
baseDate Date 認可設定画面で選択された基準日が格納されています。 2013 Winter

サブジェクトタイプ sample-prof-keep-alive では <viewPage> タグを使用してボタンの代わりにjsspページを読み込んでいます。このタグを指定するとボタンの代わりに指定されたjsspページをインクルードして表示します。ここでは次のようなページを読み込んでいます。

<imart type="imuiTextbox" id="sample-prof-keep-alive-value" style="text-align:right" size="2" />カ月以上有効なユーザ
<imart type="imuiButton" id="sample-prof-keep-alive" class="imui-small-button" style="width:118px" value="追加" />

ボタンの id 属性にサブジェクトタイプと同じ値を指定しておくことで、plugin.xmlの <button> タグに指定した action 属性の関数がクリック時のイベントハンドラとして追加されます。

これらを配備することで 「対象者の条件設定画面」に追加したサブジェクトタイプ用のボタンを追加できます。ただし、この状態で認可設定画面を開いてもこれらのボタンは表示されません。

次に説明するポリシー部分編集設定で、使用するサブジェクトタイプの設定を変更しなければなりません。

ポリシー部分編集設定

ポリシー部分編集設定はポリシーの編集に当たって使用できるサブジェクトタイプを宣言しています。

認可設定画面部品の使用 でも登場していますがサンプルアプリケーションでは以下のように定義しています。

%CONTEXT_PATH%/WEB-INF/conf/authz-partial-policy-edit-config/authz-ext-sample.xml
<?xml version="1.0" encoding="UTF-8"?>
<authz-partial-policy-edit-config
    xmlns="http://www.intra-mart.jp/authz/authz-partial-policy-edit-config"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.intra-mart.jp/authz/authz-partial-policy-edit-config ../../schema/authz-partial-policy-edit-config.xsd">

  <part-config>
    <part-id>im_authz_ext_sample</part-id>
    <caption-cd>CAP.Z.SAMPLE.AUTHZ.PARTCONFIG.TITLE</caption-cd>
    <resource-groups>
      <resource-group-id>sample-authz-data-crud</resource-group-id>
    </resource-groups>
    <subject-types>
      <subject-type-id>sample-prof-keep-alive</subject-type-id>
      <subject-type-id>sample-resource-owner</subject-type-id>
    </subject-types>
  </part-config>

</authz-partial-policy-edit-config>

この設定ファイルで使用するサブジェクトタイプIDとして追加しておかないと、認可設定画面で条件の追加を行おうとしても表示されません。

ここではもう一つ、例として標準でインストールされる「画面・処理」の認可設定に上記のサブジェクトタイプを追加してみます。

次のファイルを編集します。

%CONTEXT_PATH%/WEB-INF/conf/authz-partial-policy-edit-config/im_authz_impl_router.xml

以下のコメントで示してある行を追加します。

<?xml version="1.0" encoding="UTF-8"?>
<authz-partial-policy-edit-config
    xmlns="http://www.intra-mart.jp/authz/authz-partial-policy-edit-config"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.intra-mart.jp/authz/authz-partial-policy-edit-config ../../schema/authz-partial-policy-edit-config.xsd">

  <part-config>
    <part-id>im_authz_impl_router</part-id>
    <caption-cd>CAP.Z.IWP.ROUTER.AUTHZ.PARTCONFIG.TITLE</caption-cd>
    <resource-groups>
      <resource-group-id>http-services</resource-group-id>
    </resource-groups>
    <subject-types>
      <subject-type-id>im_authz_meta_subject</subject-type-id>
      <subject-type-id>imm_user</subject-type-id>
      <subject-type-id>imm_company_post</subject-type-id>
      <subject-type-id>imm_department</subject-type-id>
      <subject-type-id>imm_public_grp</subject-type-id>
      <subject-type-id>imm_public_grp_role</subject-type-id>
      <subject-type-id>b_m_role</subject-type-id>
      <subject-type-id>sample-prof-keep-alive</subject-type-id> <!----- この行を追加 ----------->
      <subject-type-id>sample-resource-owner</subject-type-id> <!----- この行を追加 ----------->
    </subject-types>
  </part-config>

</authz-partial-policy-edit-config>

確認

実際にサブジェクトが使用できることを確認します。

  1. これまでの資材をすべて配置した上でアプリケーションサーバを起動します。

  2. テナント管理者でログインし、認可設定画面を開きます。

  3. 「リソースの種類」が「画面・処理」となっている事を確認して「権限設定を開始する」をクリックします。

  4. 「条件の新規作成」が表示されるのでクリックすると「対象者の条件設定画面」が開き、「XXX カ月以上有効なユーザ」と「リソースオーナー」のボタンが表示されていることを確認できます。

  5. 「リソースオーナー」の方はここでは実質使用できませんので、「XXX カ月以上有効なユーザ」の方に 3 と入力して「追加」ボタンをクリックします。

    左側の対象者の欄に「3 カ月以上有効なユーザ」が追加され、名称も自動的に「3 カ月以上有効なユーザ」となっている事を確認します。

  6. 「OK」ボタンをクリックし、認可設定画面に戻ります。

    条件として「3 カ月以上有効なユーザ」が追加されていることを確認します。

  7. 同様に操作して「6 カ月以上有効なユーザ」を追加します。

  8. IM共通マスタのユーザのメンテナンス画面を開き、テナント管理者のプロファイルを開きます。

  9. 画面上の期間バーを操作して、現在の期間の開始日を4か月前にします。

  10. 一度ログアウトし、ログインしなおします。

  11. 認可設定画面を開くと、「3 カ月以上有効なユーザ」が赤く表示され、「6 カ月以上有効なユーザ」が暗く表示されていることが確認できます。

コラム

認可設定画面ではログイン時に解決されたサブジェクトが赤く表示されます。ここではテナント管理者のプロファイルの期間を4か月で切ったため、3か月以上有効なユーザとして識別されていますが、6か月以上有効なユーザとしては識別されていないことを示しています。