Keycloakのクライアント・アダプターで認可サービスを試してみる（Tomcat編）

# 今日やること

昨年の [Keycloak by OpenStandia Advent Calendar 2017](https://qiita.com/advent-calendar/2017/keycloak-by-openstandia) で、[Keycloakのクライアント・アダプター(Tomcat)](https://qiita.com/yagiaoskywalker/items/c7e9433734f2880b1aa7)に関する記事を書きましたが、当時はKeycloak側で行える認可サービス(アクセス制御)について言及ができていませんでした。そのため、今回の記事ではこの認可サービスに焦点をあてて続編を書こうと思います。

まず、Keycloakの認可サービスについて簡単に説明しておきます。
Keycloak の認可サービスでは、大きく分けて2つのアクセス制御方式があります。

 * システム管理者によってアクセス制御を集中管理する方式
 * エンド・ユーザー(リソース・オーナー)自身でアクセス制御を行う方式

前者はいわゆる従来型のアクセス制御方式のことで、システム管理者がアクセス制御を集中管理する方式のことです。後者は、システム管理者だけにアクセス制御を任せるのではなく、エンド・ユーザー(リソース・オーナー)自身にもアクセス制御を行えるようにする比較的新しい方式で、UMA(User-Managed Access)と呼ばれます。UMA について解説すると、この記事がそれだけで終わってしまいそうなので、ここでは詳細は割愛させてもらいます。興味がある方は、UMA の標準化団体である Kantara Initiative が公開している仕様をご参照ください。

　参考： [User-Managed Access (UMA) 2.0 Grant for OAuth 2.0 Authorization](https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html)

Keycloak では、どちらのアクセス制御方式においても、この UMA の仕様に基づいた実装が行われていますが、前者のアクセス制御方式に関しては、UMA をカスタマイズしたKeycloakの独自の方式になっており、今回の記事ではこのアクセス制御方式について検証しています。クライアント・アダプターに対して認可の設定を行うことにより、Keycloak側から各リソースに対して、以前より細やかなアクセス制御を行えることがわかると思います。

今回も昨年に引き続き、Tomcat 8のサンプルアプリ(examples)に対してクライアント・アダプター(OpenID Connect版)を導入し、どのようにアクセス制御の集中管理を行なうのかを試してみます。

# Keycloak の認可サービスについて

## 認可サービスの関連用語

今回の記事の中で、頻繁に登場する用語の意味を簡単に説明しておきます。

| 用語(英語表記) | 説明 |
|:---|:---|
|リソース(Resource)|認可サービスによるアクセス制御で保護対象となるURIを定義したもの|
|ポリシー(Policy)|認可サービスで利用するアクセス制御メカニズムを定義したもの|
|アクセス権(Permission)|定義したリソースと定義したポリシーを関連付けたもの|
|RPT(Requesting Party Token)|認可サーバーが発行する認可サービス関連の情報を含んだ OAuth 2.0 アクセス・トークンのこと|
|認可サーバー(Authorization Server)|クライアントの要求に応じて、RPTを発行するサーバーのこと。ここでは Keycloak を指します|
|リソース・サーバー(Resource Server)|リソース要求に答えるサーバー。ここでは Tomcat を指します|
|クライアント(Client)|リソースの要求を代理できるアプリケーション。ここではクライアント・アダプターを指します|
|リソース・オーナー(Resource Owner)|リソースへのアクセスを許可する対象のこと。ここではブラウザ（エンド・ユーザー）を指します|


## Keycloakで利用できるアクセス制御メカニズム

Keycloakの認可サービスでは以下の7つの方式によるアクセス制御が可能です。

| メカニズム | 説明 | ポリシーとの対応関係 |
|:---|:---|:---|
|属性ベースのアクセス制御(ABAC)|ユーザーの属性値によるアクセス制御|JavaScriptポリシー|
|ロールベースのアクセス制御(RBAC)|ユーザーの所属するロールもしくはグループによるアクセス制御|Role/Group/JavaScriptポリシー|
|ユーザーベースのアクセス制御(UBAC)|特定のユーザーにのみ適用されるアクセス制御|Userポリシー|
|コンテキストベースのアクセス制御(CBAC)|コンテキスト(ユーザーの OAuth2 アクセス・トークンや実行環境)によるアクセス制御|JavaScriptポリシー|
|ルールベースのアクセス制御|JavaScript や JBoss Drools(ビジネスルール管理システム)を利用したアクセス制御|JavaScript/Rulesポリシー|
|時間ベースのアクセス制御|特定の日付・時間によるアクセス制御|Timeポリシー|
|SPIによるカスタム・アクセス・コントロール機構（ACMs）のサポート|独自のポリシー実装にも対応します<br>※ この記事では言及しません|-|

各メカニズムを利用したアクセス制御を実現する場合には、対応するポリシーを定義することで、利用が可能になります。
各リソース(URI)に対して、

 * 単一もしくは複数のポリシーを利用
 * 複数のポリシーがある場合、その AND/OR 条件にするか、多数決にするか
 * ポリシーの判定結果の反転

などを自由に組み合わせることが可能なので、web.xmlベースのものより遥かに複雑なアクセス制御が実現可能です。


## 今回のアクセス制御方式の構成

今回のアクセス制御方式の構成は以下のようになっております。

![今回の構成.png](https://qiita-image-store.s3.amazonaws.com/0/209129/8165232c-2a47-edea-d0dd-e37fabc8cd0d.png)

アクセス制御の処理の流れは以下のようになります。

1. 認証済みのエンド・ユーザーが、/examples アプリの保護リソースのいずれかにアクセスします。
2. クライアント・アダプターに同梱されている Policy Enforcerは、エンド・ユーザーが該当リソースのアクセス権(RPT)を持っているかチェックします。RPT がまだない場合は、Policy Enforcer は Keycloak のトークンエンドポイントに対して RPT の要求を行います。
3. Keycloak は、システム管理者により定義された認可設定を参照して、アクセスしてきたエンド・ユーザーに当該リソースへのアクセス権があるかどうかの結果を返します。(許可の場合は、RPTが発行されます)
4. Policy Enforcer は、Keycloak からのアクセス可否の結果を元に、エンド・ユーザーが要求したリソースを返すか、403 応答を返します。


## Keycloak による認可サービスの主なシーケンス

認可サービスによるアクセス制御に関連するシーケンス図は以下のとおりとなります。

![Keycloak による認可サービスの主なシーケンス.png](https://qiita-image-store.s3.amazonaws.com/0/209129/537cf7dc-c0df-8b52-2b89-badb96854be5.png)



| 項番 | サービスエンドポイント | タイミング | 用途 |
|:---|:---|:---|:---|
|(1)|/auth/realms/{レルム名}/.well-known/uma2-configuration|アダプター起動時|認可サービスで使用される各種エンドポイントを取得|
|(2)|/auth/realms/{レルム名}/protocol/openid-connect/token<br><br>POST パラメータ:<br>　・grant_type=client_credentials|アダプター起動時|クライアント認証によるアクセス・トークンの取得|
|(3)|/auth/realms/{レルム名}/authz/protection/resource_set|アダプター起動時|リソースの一覧(リソースIDの配列)を取得|
|(4)|/auth/realms/{レルム名}/authz/protection/resource_set/{リソースID}|アダプター起動時|リソースの詳細(名前、URI、タイプなど)を取得|
|(5)|/auth/realms/{レルム名}/protocol/openid-connect/token<br><br>POST パラメータ(抜粋):<br>　・audience={クライアントID}<br>　・grant_type=urn:ietf:params:oauth:grant-type:uma-ticket<br>　・permission={チェックを行うアクセス権ID}<br>　・rpt={取得済みのRPT}|ユーザーアクセス時|リソースに応じたアクセス可否の問い合わせ。<br>PERMIT(許可)であれば、200応答で RPT(Requesting Party Token) が得られます。<br>DENY(拒否)であれば、403応答になります。<br><br>※ 初回アクセス時は、permission や、rpt パラメータは指定されず、すべてのアクセス権に対する可否が事前にチェックされる動きになります|

この認可サービスのシーケンス図を見ると

 * クライアント・アダプターの初期化時に認可サービス関連の設定や保護されるリソース情報を取得する
 * リソース・オーナーがリソースにアクセスした時にクライアント・アダプターが認可サーバーにアクセス可否を問い合わせる

という処理が増えていることがわかります。

:information_source: (5) の RPT を取得するための認可リクエストですが、UMA の仕様と Keycloak の実装では若干差があります。UMA では、grant_type と ticket が必須属性ですが、Keycloak では grant_type のみが必須で、ticket は必須ではありません。また、Keycloak では、audience や permission といった UMA の仕様にはないパラメータも使われています。詳細につきましては、以下の双方の認可リクエストの仕様をご参照ください。

　参考： [Keycloakの認可リクエストの仕様](https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_obtaining_permissions)
　参考： [UMAの認可リクエストの仕様](https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.3.1)

:information_source: (5) のリクエストは初回アクセス時には必ず発生しますが、PERMITの結果が得られたリソースに関しては、一定時間再呼び出しが行われなくなります。そのため、タイミングにより評価結果がPERMITからDENYに切り替わるようなポリシーを利用している場合、拒否されるまでに若干のタイミングのずれが生じることがあります。この点については後述します。


# 動作確認用のサーバー構成

[去年の記事](https://qiita.com/yagiaoskywalker/items/c7e9433734f2880b1aa7)をベースにミドルウェアのバージョンを上げた環境で動作確認しました。去年の古いバージョンでは認可サービスが正しく動かない箇所があったため、実機で確認される方はここに記載したバージョン以上の環境で確認していただくことを推奨します。

## 環境情報
|FQDN|OS|JDK|構成|
|:---|:---|:---|:---|
|kc-server.example.com|CentOS 7.5.1804|OpenJDK 1.8.0_191|・Keycloakサーバー 4.5.0.Final|
|kc-tomcat.example.com|CentOS 7.5.1804|OpenJDK 1.8.0_191|・Tomcat 8.5.35(examplesアプリを利用)<br>・Tomcat 8用クライアント・アダプター(OpenID Connect版) 4.5.0.Final|

## ユーザー定義
|ユーザー|メール|ロール|グループ|
|:---|:---|:---|:---|
|user001|user001@example.com|user|openstandia|
|user002|user002@example.com|user|nri|
|user003|user003@test.example.com|user|top|
|admin001|admin001@example.com|admin|openstandia|
|admin002|admin002@example.com|admin|nri|
|admin003|admin003@example.com|admin|top|

## グループ定義
 * top
   * nri
     * openstandia

## アクセス制御設計
Tomcatに付属するexamplesアプリケーションで、以下のようなアクセス制御をかけることを想定します。この表を見てわかるとおり、認可サービスを利用すると、より複雑なポリシーが定義できることが分かりますね。

|URI|アクセス制御の設計|利用ポリシー|
|:---|:---|:---|
|/*|フルアクセス(デフォルト)|JavaScriptポリシー(デフォルト)|
|/jsp/*|・サービス時間(9時～18時)内<br>もしくは<br>・adminロールのみ|Time/Roleポリシー|
|/jsp/jsp2/el/*|user001のみ|Userポリシー|
|/jsp/jsp2/simpletag/*|属性に特定の値を持つユーザー|JavaScriptポリシー|
|/jsp/tagplugin/*|・ランチタイム(12時～13時)以外<br>かつ<br>・nri グループ配下|Time/Groupポリシー|
|/websocket/*|・サービス時間(9時～18時)内<br>・openstandiaグループ<br>・userロール<br>での多数決|Time/Group/Role/Aggregatedポリシー|


# Keycloakサーバー側の設定

以下、去年の環境からの変更点のみ記載します。

## 認可制御の有効化
1. Keycloakの管理コンソールにログインします。
1. 左メニューバーから、`demo`レルムを選択します。
1. 左メニューバーで `クライアント`をクリックします。クライアント一覧が表示されます。
2. クライアント一覧から `kc-tomcat`を選択します。
3. `認可の有効`のトグルスイッチをオンにします。
4. `保存`ボタンを押下します。
1. `認可`タブを押下します。

![setting_01.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/6d9da1c0-41c9-2b1c-d775-5e71d5e1c30c.png)

## リソースの定義

認可サービスにより、アクセス制御を行うリソース群をすべて定義します。

1. `リソース`タブを押下します。
1. `作成`ボタンを押下します。
1. 以下の表の設定に従い、7つのリソース定義を行います。

|名前|タイプ|URI|
|:---|:---|:---|
|Default Resource|urn:kc-tomcat:resources:default|/*|
|jsp Resource||/jsp/*|
|el Resource||/jsp/jsp2/el/*|
|simpletag Resource||/jsp/jsp2/simpletag/*|
|tagplugin Resource||/jsp/tagplugin/*|
|websocket Resource||/websocket/*|

:information_source: `Default Resource` はデフォルトで作成される`リソース`です。
:information_source: `URI`はコンテキストパス以降のパスを指定します。複数指定することも可能です。
:information_source: `タイプ`を指定してリソースをグループ化することができます。同じポリシーを適用したいリソースが多数ある場合などは、タイプを設定しておく方がのちのちの設定が楽になります。

jsp Resource の設定例：
![setting_02.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/f1aad39f-db64-6902-cac4-7738413266f6.png)

## ポリシーの定義
各リソースに適用するためのポリシー群をすべて定義します。

### Roleポリシーの定義

特定のロールを持つユーザーだけにアクセスを限定するためのポリシーです。

1. `ポリシー`タブを押下します。
1. `ポリシーを作成...`のプルダウンから、`Role`を選択します。
1. 以下の表の設定に従い、2つのRoleポリシー定義を行います。

|名前|レルムロール|必須|
|:---|:---|:---|
|rp_user|user|チェックあり|
|rp_admin|admin|チェックあり|

rp_user の設定例：
![setting_03.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/34e7b42b-c39d-c538-7c08-4fa4a83527c4.png)


### JavaScriptポリシーの定義

JavaScriptコードによりコンテキストの内容を判定し、アクセスを限定するためのポリシーです。
今回はメールアドレスのドメイン部に "@test.example.com" が含まれるかどうかをチェックしようと思います。

1. `ポリシー`タブを押下します。
1. `ポリシーを作成...`のプルダウンから、`JavaScript`を選択します。
1. `名前`に`jp_testuser`を設定し、以下の`コード`を入力して保存します。

```javascript
var context = $evaluation.getContext();
var identity = context.getIdentity();
var attributes = identity.getAttributes();
var email = attributes.getValue('email').asString(0);

// ロギング
print("email : " + email);

// email に "@test.example.com" という値が入っていれば許可
if (email.contains("@test.example.com")) {
    print("JavaScript Policy Grant!");
    $evaluation.grant();
} else {
    print("JavaScript Policy No Grant!");
}
```
:information_source: `$evaluation.grant()` が実行されなかった場合には、DENY の判定になります。 
:information_source: print 文により、ロギングできます。処理の流れを確認するためのデバッグなどで有用です。

jp_testuser の設定例：
![setting_04.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/0d40e5d9-bc93-120c-8aca-fe88f0712234.png)


### Timeポリシーの定義

特定の時間帯だけにアクセスを限定するためのポリシーです。

1. `ポリシー`タブを押下します。
1. `ポリシーを作成...`のプルダウンから、`Time`を選択します。
1. 以下の表の設定に従い、2つのTimeポリシー定義を行います。

|名前|時(from)|時(to)|分(from)|分(to)|ロジック|
|:---|:---|:---|:---|:---|:---|
|tp_not_lunch-time|12|12|0|59|**Negative**|
|tp_service-time|9|17|||Positive|

tp_not_lunch-time は、ランチタイム以外という条件にしたいので、`ロジック`で`Negative`を選んで条件を反転させます。

tp_not_lunch-time の設定例：
![setting_05.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/27b64038-9fb5-491a-9481-4d03d9ced45b.png)


### Userポリシーの定義

特定のユーザーだけにアクセスを限定するためのポリシーです。

1. `ポリシー`タブを押下します。
1. `ポリシーを作成...`のプルダウンから、`User`を選択します。
1. 以下の表の設定に従い、2つのUserポリシー定義を行います。

|名前|ユーザー|
|:---|:---|
|up_user001|user001|
|up_admin001|admin001|

up_user001 の設定例：
![setting_06.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/58706de7-0b0c-60f6-e37d-d7d28c45bc88.png)



### Groupポリシーの定義

特定のグループだけにアクセスを限定するためのポリシーです。
`Extend to Children` をチェックすれば、グループの親子関係も考慮されます。

1. `ポリシー`タブを押下します。
1. `ポリシーを作成...`のプルダウンから、`Group`を選択します。
1. 以下の表の設定に従い、2つのGroupポリシー定義を行います。

|名前|グループ|Extend to Children|
|:---|:---|:---|
|gp_nri|nri|**チェックあり**|
|gp_openstandia|openstandia|チェックなし|

gp_nri の設定例：
![setting_07.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/4b63b97b-3a25-5a02-d682-a06f65edaef3.png)


### Aggregatedポリシーの定義

複数のポリシーを集約するポリシーためのポリシーです。

1. `ポリシー`タブを押下します。
1. `ポリシーを作成...`のプルダウンから、`Aggregated`を選択します。
1. 以下の表の設定に従い、1つのAggregatedポリシー定義を行います。

|名前|ポリシーの適用|判定戦略|
|:---|:---|:---|
|ap_consensus|・tp_service-time<br>・gp_openstandia<br>・rp_user|Consensus|

Aggregatedポリシーでは複数ポリシーを組み合わせるため、以下の`判定戦略`の中から1つを選択する必要があります。ここでは、多数決の動作にしたいので、`Consensus`を選択します。

|判定戦略|説明|
|:---|:---|
|Affirmative|個々のポリシー判定のOR条件|
|Unanimous|個々のポリシー判定のAND条件|
|Consensus|個々のポリシー判定の多数決。PERMIT／DENYが同数の場合は許可されません。|


ap_consensus の設定例：
![setting_08.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/e5fdd05b-1077-5325-7020-c4a68e597cf9.png)


## アクセス権の定義

定義したリソースと定義したポリシーを組み合わせて、実際の挙動に反映されるアクセス権を定義します。

1. `アクセス権`タブを押下します。
1. `ポリシーを作成...`のプルダウンから、`Resource-Based`を選択します。
1. 以下の表の設定に従い、7つのアクセス権の定義を行います。

|名前|リソースタイプに適用|リソースorタイプ|ポリシーの適用|判定戦略|
|:---|:---|:---|:---|:---|
|Default Permission|ON|urn:kc-tomcat:resources:default|Default Policy|Unanimous|
|jsp Permission|OFF|jsp Resource|・tp_service-time<br>・rp_admin|**Affirmative**|
|el Permission|OFF|el Resource|up_user001|Unanimous|
|simpletag Permission|OFF|simpletag Resource|jp_testuser|Unanimous|
|tagplugin Permission|OFF|tagplugin Resource|・tp_not_lunch-time<br>・gp_nri|Unanimous|
|websocket Permission|OFF|websocket Resource|ap_consensus|Unanimous|

`jsp Permission` は、2つのポリシーのOR条件で判定したいので、判定戦略は`Affirmative`を指定します。

:information_source: `Default Policy`と`Default Permission` と、はデフォルトで作成されている`ポリシー`と`パーミッション`です。`Default Policy`は、JavaScriptポリシーになっており、無条件でアクセスを許可するコードになっています。

jsp Permission の設定例：
![setting_09.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/b5fe86d3-bc4d-4c50-5707-54d243c138d4.png)


# クライアント・アダプター側の設定

## WEB-INF/keycloak.json の変更

Keycloakによる認可サービスを有効にするため、WEB-INF/keycloak.json に policy-enforcer の設定を追加します。

```json:/usr/local/apache-tomcat-8.5.35/webapps/examples/WEB-INF/keycloak.json
{
  "realm": "demo",
  "auth-server-url": "https://kc-server.example.com:8443/auth",
  "ssl-required": "none",
  "resource": "kc-tomcat",
  "credentials": {
    "secret": "{Keycloakサーバー側のシークレットの値}"
  },
  "policy-enforcer": {}
}
```

:information_source: ここでは、policy-enforcer に何も設定を足していませんが、たとえば、[Claim Information Point](https://www.keycloak.org/docs/latest/authorization_services/index.html#_enforcer_claim_information_point) の設定を追加すれば、リソース・オーナーからリクエストされた際のHTTPメソッド、URI、HTTPヘッダー(クッキーやユーザーエージェントなど)、パラメータ、ボディーなど様々な値をポリシーの判定に利用できます。

## WEB-INF/web.xmlの変更

今回はKeycloakによる認可サービスを実施するため、以前実施していた web.xml でのロールによるアクセス制御部分はすべて解除して、認証済みユーザーはすべてのURLにフルアクセスできるように設定を変更します。

```xml:/usr/local/apache-tomcat-8.5.35/webapps/examples/WEB-INF/web.xml
    ・
    ・
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Protected Area</web-resource-name>
            <url-pattern>/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>*</role-name>
        </auth-constraint>
    </security-constraint>

    <login-config>
        <auth-method>BASIC</auth-method>
        <realm-name>this is ignored currently</realm-name>
    </login-config>

    <security-role>
        <role-name>*</role-name>
    </security-role>
    ・
    ・
```

## Tomcatの起動

クライアント・アダプターも準備が整ったので、Tomcat再起動します。

```bash
cd /usr/local/apache-tomcat-8.5.35/
./bin/shutdown.sh
./bin/startup.sh
```

# 動作確認

実際にTomcatにアクセスして動作を確認してもよいですが、Keycloak 管理コンソールには、`評価`タブという便利な機能があるので、こちらでポリシー評価の結果を確認したいと思います。

1. Keycloakの管理コンソールにログインします。
1. 左メニューバーから、`demo`レルムを選択します。
1. 左メニューバーで `クライアント`をクリックします。クライアント一覧が表示されます。
2. クライアント一覧から `kc-tomcat`を選択します。
1. `認可`タブを押下します。
1. `評価`タブを押下します。

![setting_10.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/1b558263-f4dd-1b1c-7c78-fc423b406ea7.png)

:information_source: `評価`タブでの注意事項

- これ以降のアクセス制御の判定結果の表では、複数のユーザーの評価結果をまとめて書いてますが、実際の`評価`タブでは1ユーザーずつしか確認できません。複数の`リソース`に対する評価を同時に判定することは可能です。
- 評価を行う`リソース` はプルダウンから選択後に、`Add`ボタンを押下して、評価対象として追加する必要があります。

jsp Resource が `Add` される前の表示例：
![setting_11.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/cccae429-9bb4-4354-ec71-f865f61d6d3e.png)


jsp Resource が `Add` された後の表示例：
![setting_12.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/892615ac-b3b4-1ba5-daa4-104511094514.png)


## `jsp Resource` へのアクセス

>■ 設計したアクセス制御ロジック
>・サービス時間(9時～18時)内
> もしくは
>・adminロールのみ

1. 以下の条件を入力して、`評価`ボタンを押下します。

 * 10:00(サービス時間内)に評価した場合

|ユーザー|リソース||判定|
|:---|:---|:---|:---|:---|
|user001|jsp Resource|=>|PERMIT|
|user002|jsp Resource|=>|PERMIT|
|user003|jsp Resource|=>|PERMIT|
|admin001|jsp Resource|=>|PERMIT|
|admin002|jsp Resource|=>|PERMIT|
|admin003|jsp Resource|=>|PERMIT|




判定戦略が `Affirmative`(ポリシーのOR条件) なので、サービス時間内もしくは、adminロールを持っていればアクセスが許可されます。そのため、サービス時間内であれば、どのユーザーであってもアクセスが許可されます。

user001 で評価した場合の例:
![jspPERMIT_user001.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/63b414de-b872-0801-fd75-7cf195f1f65d.png)


 * 19:00(サービス時間外)に評価した場合

|ユーザー|リソース||判定|
|:---|:---|:---|:---|:---|
|user001|jsp Resource|=>|**DENY**|
|user002|jsp Resource|=>|**DENY**|
|user003|jsp Resource|=>|**DENY**|
|admin001|jsp Resource|=>|PERMIT|
|admin002|jsp Resource|=>|PERMIT|
|admin003|jsp Resource|=>|PERMIT|

サービス時間外では、adminロールをもっていないユーザーはアクセスが拒否されます。

user001 で評価した場合の例:
![jspDENY_user001.png](https://qiita-image-store.s3.amazonaws.com/0/209129/a0a57b18-a786-d77c-54c1-1c067f74d894.png)


## `el Resource` へのアクセス

>■ 設計したアクセス制御ロジック
>・user001のみ

1. 以下の条件で、`評価`ボタンを押下します。

 * 10:00(サービス時間内)に評価した場合

|ユーザー|リソース||判定|
|:---|:---|:---|:---|:---|
|user001|el Resource|=>|PERMIT|
|user002|el Resource|=>|**DENY**|
|user003|el Resource|=>|**DENY**|
|admin001|el Resource|=>|**DENY**|
|admin002|el Resource|=>|**DENY**|
|admin003|el Resource|=>|**DENY**|

user001のみがアクセスが許可されます。
評価結果をみると、上位リソース(/* や /jsp/*)のポリシーについては全く判定されていないことがわかります。

user001 で評価した場合の例:
![elPERMIT_user001.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/01cb60ac-6439-ba79-be0e-191b11e7996c.png)



 * 19:00(サービス時間外)に評価した場合

|ユーザー|リソース||判定|
|:---|:---|:---|:---|:---|
|user001|el Resource|=>|PERMIT|
|user002|el Resource|=>|**DENY**|
|user003|el Resource|=>|**DENY**|
|admin001|el Resource|=>|**DENY**|
|admin002|el Resource|=>|**DENY**|
|admin003|el Resource|=>|**DENY**|

アクセス時刻によって、評価結果が変わってないことがわかります。しつこいようですが、`/jsp/jsp2/el/*` にアクセスした場合は、上位リソース(/* や /jsp/*)のポリシーについては評価の対象になりません。上位のポリシーも考慮されると勘違いしてしまうと、誤ったアクセス制御になってしまうことがあるので注意が必要です。

user001 で評価した場合の例:
![elPERMIT_user001.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/92af96b9-fb7a-612b-41fa-3240094f167a.png)

## `simpletag Resource` へのアクセス

>■ 設計したアクセス制御ロジック
>属性に特定の値を持つユーザー

|ユーザー|リソース||判定|
|:---|:---|:---|:---|:---|
|user001|simpletag Resource|=>|**DENY**|
|user002|simpletag Resource|=>|**DENY**|
|user003|simpletag Resource|=>|PERMIT|
|admin001|simpletag Resource|=>|**DENY**|
|admin002|simpletag Resource|=>|**DENY**|
|admin003|simpletag Resource|=>|**DENY**|

email 属性に "@test.examle.com" が含まれる user003 だけアクセスが許可されます。

user003 で評価した場合の例:
![simplePERMIT_user003.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/72fe5914-8daf-c6f4-93a8-b186b56d097b.png)


## `tagplugin Resource` へのアクセス

>■ 設計したアクセス制御ロジック
>・ランチタイム(12時～13時)以外
> かつ
>・nri グループ配下

1. 以下の条件で、`評価`ボタンを押下します。

 * 10:00（ランチタイム以外)に評価した場合

|ユーザー|リソース||判定|
|:---|:---|:---|:---|:---|
|user001|tagplugin Resource|=>|PERMIT|
|user002|tagplugin Resource|=>|PERMIT|
|user003|tagplugin Resource|=>|**DENY**|
|admin001|tagplugin Resource|=>|PERMIT|
|admin002|tagplugin Resource|=>|PERMIT|
|admin003|tagplugin Resource|=>|**DENY**|

判定戦略が `Unanimous`(ポリシーのAND条件) なので、ランチタイム以外かつ、nriグループ(子グループも含む)のユーザーだけがアクセスが許可されます。
user001/admin001 は、openstandiaグループですが、openstandiaグループはnriグループの子グループなのでアクセスが許可されます。
user003/admin003  は、nriグループではないので、ランチタイム以外であっても、アクセスが拒否されます。

user003 で評価した場合の例:
![tagpluginDENY_user003.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/0fbde98d-498f-8a9a-3a81-1b9be9784b5c.png)


 * 12:30（ランチタイム内)に評価した場合

|ユーザー|リソース||判定|
|:---|:---|:---|:---|:---|
|user001|tagplugin Resource|=>|**DENY**|
|user002|tagplugin Resource|=>|**DENY**|
|user003|tagplugin Resource|=>|**DENY**|
|admin001|tagplugin Resource|=>|**DENY**|
|admin002|tagplugin Resource|=>|**DENY**|
|admin003|tagplugin Resource|=>|**DENY**|

判定戦略が `Unanimous`なので、ランチタイム内だとどのようなユーザーであっても、アクセスが拒否されます。

user001 で評価した場合の例:
![tagpluginDENY_user001.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/9dd134ab-840d-24e0-3ab1-958be6f9b4aa.png)


## `websocket Resource` へのアクセス

>■ 設計したアクセス制御ロジック
>・サービス時間(9時～18時)内
>・openstandiaグループ
>・userロール
>での多数決

1. 以下の条件で、`評価`ボタンを押下します。

 * 10:00(サービス時間内)に評価した場合

|ユーザー|リソース||判定|
|:---|:---|:---|:---|:---|
|user001|websocket Resource|=>|PERMIT|
|user002|websocket Resource|=>|PERMIT|
|user003|websocket Resource|=>|PERMIT|
|admin001|websocket Resource|=>|PERMIT|
|admin002|websocket Resource|=>|**DENY**|
|admin003|websocket Resource|=>|**DENY**|

Aggregated ポリシーの判定戦略が `Consensus`(ポリシーの多数決) なので、今回の設定では3つのポリシーのうち2つ以上が `PERMIT`となれば、アクセスが許可されます。そのため、サービス時間内であれば、userロールを持っている user00\* は、全員アクセスが許可されます。
admin00\* では、openstandiaグループである admin001 だけがアクセスが許可されます。

admin001 で評価した場合の例:
![websocketPERMIT.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/0dccb3ad-e96a-51e3-d907-c9e13dc8ef44.png)


 * 19:00(サービス時間外)に評価した場合

|ユーザー|リソース||判定|
|:---|:---|:---|:---|:---|
|user001|websocket Resource|=>|PERMIT|
|user002|websocket Resource|=>|**DENY**|
|user003|websocket Resource|=>|**DENY**|
|admin001|websocket Resource|=>|**DENY**|
|admin002|websocket Resource|=>|**DENY**|
|admin003|websocket Resource|=>|**DENY**|

サービス時間外の場合は、userロールかつopenstandiaグループである user001 しかアクセスが許可されません。
サービス時間内は許可されていた admin001 は拒否されます。

admin001 で評価した場合の例:
![websocketDENY.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/7c859abc-22d8-6abc-59b6-1de1ceb60de1.png)



:information_source: Aggregated ポリシーを使った場合、個々のポリシーの判定結果がどうであったかは出力されないので、ぱっと見ではなぜそうなったのかの判断が難しくなります。

## `Default Resource` へのアクセス

リソースに定義した以外の URI(/servlets/\* など)にアクセスした場合は、`Default Resource` へのアクセスとみなされるので、`Default Permission` により評価される動きになります。

>■ 設計したアクセス制御ロジック
>フルアクセス(デフォルト)

1. 以下の条件で、`評価`ボタンを押下します。

|ユーザー|リソースタイプ||判定|
|:---|:---|:---|:---|:---|
|user001|Default Resource|=>|PERMIT|
|user002|Default Resource|=>|PERMIT|
|user003|Default Resource|=>|PERMIT|
|admin001|Default Resource|=>|PERMIT|
|admin002|Default Resource|=>|PERMIT|
|admin003|Default Resource|=>|PERMIT|

特に条件なく、すべてのユーザーのアクセスが許可されます（Keycloakへのログインは必要です)。

user001 で評価した場合の例:
![defaultPERMIT_user001.PNG](https://qiita-image-store.s3.amazonaws.com/0/209129/ed19d1f3-8139-a733-a64c-a21cf08478be.png)

# アクセス制御の拒否のタイミングが遅延するパターン

上記の`評価`タブでは適切な応答を返してくることが確認できますが、実際にクライアント・アダプターを使った動作ではアクセス拒否のタイミングが遅延するパターンがあったので、ここに書いておきます。

前にも少し触れましたが、認可サービスのシーケンス図で示した (5) のアクセス可否を判定するためのリクエストは、毎回発行されるわけではありません。(5) の応答が DENY になったアクセス権に関しては、都度、再問い合わせが行われる動きになりますが、一度、PERMIT になったアクセス権については、5分間は再問い合わせを行いません。これは、アクセス許可となった際に発行されるRPT(Requesting Party Token)の有効期限がデフォルトでは5分間になっているためです(これはアクセス・トークンの有効期限と同様です)
そのため、Timeポリシーを使っている場合に、時間経過によりポリシーの評価がPERMITからDENYに切り替わったとしても、最大で5分間は引き続きアクセスできてしまう可能性があります。判定に遅延が起きる場合と、起きない場合のシーケンス図を以下に記載しました。

![アクセス拒否が遅延するパターン.png](https://qiita-image-store.s3.amazonaws.com/0/209129/af01c6a8-eeaf-b538-264f-348ae892d8e8.png)

![アクセス拒否が遅延しないパターン.png](https://qiita-image-store.s3.amazonaws.com/0/209129/39e0ad7d-c7df-1e1e-af6b-f41200f525b2.png)



この動き自体は、Timeポリシー以外のポリシーでも発生します。ユーザーの所属するロールやグループなどが変わって、ポリシーの判定がPERMITからDENYに変化することもあるからです。
逆に、ポリシーの判定がDENYからPERMITに変わるようなパターンではこの遅延は起きません。

RPT の有効期限を短くすればこの現象は緩和できそうです。ですがその場合、アクセス可否のチェックが増えるため、パフォーマンスに影響がでます。このあたりはどちらを優先するかのトレードオフになりそうです。
(OpenAM のWebエージェントでもポリシーキャッシュの有効期限があり似たような問題がありました)


# まとめ

Keycloak側のクライアント・アダプターの認可設定により、リソースに対して複雑なアクセス制御かける方法をおおまかに理解できました。正直なところ、ここまで複雑なアクセス制御が必要になるパターンはそれほど多くない気がしますが、アクセス制御を集中管理できる点や動的に条件を変更できる点など、いくつかのメリットを感じることができました。アプリケーションにクライアント・アダプターを導入できる環境であれば、一度試してみる価値はあると思いました。

今回、動作確認してみて少し混乱したのが、リソースのURIに親子関係がある場合の動作です。
Keycloak の認可サービスでは、リソースの URI の親子関係は考慮しません。”上位URIに設定されているポリシーは下位の URI のポリシーには影響を与えない(個々で判断される)”と理解しておく必要があります。

また、今回の記事では、認可サービスの中でも、管理者によるアクセス制御の集中管理の部分のみの検証になってしまいました。先に述べたように認可サービスには、エンド・ユーザー自身による認可ワークフローおよび、アクセス制御の管理である UMA（User-Managed Access)の機能があります。次回はこの部分について検証してみたいです。


# 参考資料
- [Keycloak Documentation](http://www.keycloak.org/documentation.html)
  - [Authorization Services Guide](https://www.keycloak.org/docs/latest/authorization_services/index.html)
- [NRI OpenStandia Keycloak日本語ドキュメント](https://keycloak-documentation.openstandia.jp/master/)
  - [Authorization Services Guide](https://keycloak-documentation.openstandia.jp/master/ja_JP/authorization_services/index.html)
- [Keycloak by OpenStandia Advent Calendar 2017](https://qiita.com/advent-calendar/2017/keycloak-by-openstandia)
    - [Keycloakのクライアント・アダプターを試してみる（Tomcat編）](https://qiita.com/yagiaoskywalker/items/c7e9433734f2880b1aa7)
- [User-Managed Access (UMA) 2.0 Grant for OAuth 2.0 Authorization](https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html)