CAPとJob Scheduling Serviceでユーザとロール情報を取得する仕組みを作る

# はじめに
この記事は [chillSAP 夏の自由研究2021](https://note.com/chillsap/n/n08ef5ec73b4c) の記事として執筆しています。

私は最近、[SAP Workflow Serviceの個人的PoC](https://qiita.com/tami/items/79e2b0bf4e0b74239d98)と称してBTPのWorkflowにからめた検証を行っています。その中で、以下のような要件が出てきました。

- ユーザが画面で指定した承認者が、適切な権限を持っているかチェックしたい
- 画面（UI5アプリ）のバックエンドであるCAPの中で上記のチェックを行う

「ログインユーザの権限」を見るのであれば、CAPの中で[スコープのチェックができます](https://cap.cloud.sap/docs/guides/authorization#4-scopes-are-narrowed-to-local-roles)。しかし、承認者として設定されたユーザはログインユーザではないので、権限チェックは簡単にはできません。

# BTPにおける権限の仕組み
BTPにおける権限の最小単位はスコープです。スコープはデータに対する読み込み、書き込みなどの権限を表します。一つまたは複数のスコープがロールテンプレートを介してロールに割り当てられます。ロールはロールコレクションを介してユーザに割り当てられます。

よって、「ユーザが特定のスコープを持っているか」をチェックする場合、ユーザ⇒ロールコレクション⇒ロール⇒スコープの順に展開する必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/0cb7b0bd-d872-4b04-8a68-2d11674c27f0.png)
図：[User Management in SAP HANA Cloud Cockpit](https://help.sap.com/viewer/6a4563286d06419cb9927ef448c67432/Cloud/en-US/aaf74e92b9a34c2ca877153edd8e35df.html)より引用

# 考えた権限チェックの仕組み
以下の図は、PoCで実現しようとしてる仕組みの全体です。CAP1はUI5アプリから呼ばれ、ワークフローを起動したり、承認、却下などのタスクを実行します。CAP2は、ユーザやロールコレクションなどのマスタデータを持ち、ユーザの権限チェックのためのAPIを提供します。CAP2を中心とする、黄色い枠で囲った部分が今回の記事で説明する範囲です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/9621aef8-c4a2-d483-7146-c3e821777fc6.png)

CAP2では、[Authorization and Trust Management Service](https://api.sap.com/package/authtrustmgmnt/rest)のAPIを使い、BTPのサブアカウントに登録されているユーザの一覧、ロールコレクション、およびロールを取得してHANA DBに格納します。この処理を、[Job Scheduling Service](https://help.sap.com/viewer/07b57c2f4b944bcd8470d024723a1631/Cloud/en-US/22c2df4d22cb4a05af4c9502a67597ae.html)を使用して日次のジョブで実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/a9e8df78-88d2-4f25-ebd1-a4ca45053bc5.png)

権限チェックでは、上記のマスタをもとに承認者に設定されたユーザが持っているスコープを取得します（下図）。ユーザやロールコレクションといったマスタの更新頻度に比べ、ユーザへのロール割り当て更新の頻度は高い（日中にも行う可能性がある）という想定で、ユーザに割り当たったロールコレクションを取得するところだけはリアルタイムに行うようにしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/8ee521b6-71dd-fe1c-dc54-dbebc0b325dd.png)

## 技術的なポイント
技術的なポイント（私が未経験だったこと）は、以下の2点です。

- **CAPからAuthorization and Trust Management ServiceのAPI（REST API）を呼ぶ**
- **Job Scheduling ServiceでCAPのアクションを呼ぶ**

# 実装方法
以下で実装方法について説明していきます。ステップは大きく分けて3つです。

1. Authorization and Trust Management ServiceのDestination登録
1. CAPサービスの実装
1. Job Scheduling ServiceからCAPのサービスを呼ぶ

## 1. Authorization and Trust Management ServiceのDestination登録
CAPからAuthorization and Trust Management ServiceのAPIを呼ぶため、サブアカウントにDestinationを登録します。

### 1.1. APIにアクセスするためのXSUAAサービスインスタンスを作成
参考：[Access UAA Admin APIs](https://help.sap.com/viewer/65de2977205c403bbc107264b8eccf4b/Cloud/en-US/ebc9113a520e495ea5fb759b9a7929f2.html)

アプリケーションにバインドするXSUAAサービスインスタンスは通常、タイプ`application`で作成しますが、ここでは`apiaccess`で作成するのがポイントです。

```
cf create-service xsuaa apiaccess <access_name>
```
以下のコマンドでサービスキーを作成します。

```
cf create-service-key <access_name> <key_name>
```
サービスキーを作ったら、以下のコマンドでサービスキーを表示させます。

```
cf service-key <access_name> <key_name>
```

### 1.2. Destinationを登録
以下の内容でDestinationを登録します。

|項目  |設定値  |
|---|---|
|Name  |任意  |
|Type  |HTTP  |
|Description  |任意  |
|URL  |サービスキーの`apiurl`  |
|Proxy Type |Internet  |
|Authentication  |OAuth2ClientCredentials  |
|Client ID  |サービスキーの`clientid`  |
|Client Secret  |サービスキーの`clientsecret`  |
|Token Service URL  |サービスキーの`url` + `/oauth/token` |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/87e568a3-4746-4bfe-755e-9eff07e46f8f.png)

## 2. CAPサービスの実装
簡単にするために、ここではUsersテーブルにデータを格納する部分に絞って説明します。
全体のコードは以下のGitリポジトリに格納しています。
https://github.com/miyasuta/cap-user-roles

### 2.1. プロジェクトを登録
以下のコマンドでCAPのプロジェクトを登録します。

```
cds init user-roles
```

### 2.1. db/schema.cds
以下がスキーマの定義です。

```
using {managed} from '@sap/cds/common';

namespace miyasuta.users;

entity Users : managed {
    key id         : UUID;
        externalId : String;
        userName   : String;
        familyName : String;
        givenName  : String;
}
```

### 2.2. srv/user-service.cds
以下がサービスの定義です。`loadUsers`というアクションで、ユーザ情報をAPIで取得してテーブルに格納します。

```
using { miyasuta.users as db } from '../db/schema';

service UserService {
    entity Users as projection on db.Users
    action loadUsers() returns String;
}
```

### 2.3. srv/user-service.js
アクションのイベントハンドラを登録します。BTPのサブアカウントに登録したDestinationを使いたいので、SAP Cloud SDKの[Generic HTTP Client](https://sap.github.io/cloud-sdk/docs/js/features/connectivity/generic-http-client)を使うのがポイントです。
参考：[Use CloudSDK for http request in NodeJS on SCP CF](https://blogs.sap.com/2020/10/23/use-cloudsdk-for-http-request-in-nodejs-on-scp-cf/)

```javascript
const core = require('@sap-cloud-sdk/core')

module.exports = async function () {
    this.on('loadUsers', async(req) => {
        const { Users } = cds.entities
        const response = await core.executeHttpRequest({ destinationName: '<1.2.で登録したDestination>'},{
            method: 'GET',
            url: '/Users'
        })

        const users = response.data.resources.map((user)=> {
            return {
                id: user.id,
                externalId: user.externalId,
                userName: user.userName,
                familyName: user.name.familyName,
                givenName: user.name.givenName
            }
        })

        if (users.length > 0) {
            await DELETE.from(Users)
            await INSERT (users) .into (Users)
            return `${users.length} records have been inserted.`
        }
        else {
            req.error({
                message: 'User data could not be fetched!'
            })
        }       
    })
}
```

### 2.4. mta.yaml
mta.yamlの設定は以下のようになります。`user-roles-srv`に、destinationとxsuaaのサービスインスタンスをバインドしています。

```yaml
## Generated mta.yaml based on template version 0.4.0
## appName = user-roles
## language=nodejs; multitenant=false
## approuter=
_schema-version: '3.1'
ID: user-roles
version: 1.0.0
description: "A simple CAP project."
parameters:
  enable-parallel-deployments: true
   
build-parameters:
  before-all:
   - builder: custom
     commands:
      - npm install --production
      - npx -p @sap/cds-dk cds build --production

modules:
 # --------------------- SERVER MODULE ------------------------
 - name: user-roles-srv
 # ------------------------------------------------------------
   type: nodejs
   path: gen/srv
   parameters:
     buildpack: nodejs_buildpack
   requires:
    # Resources extracted from CAP configuration
    - name: user-roles-db
    - name: user-roles_destination
    - name: user-roles_uaa
   provides:
    - name: srv-api      # required by consumers of CAP services (e.g. approuter)
      properties:
        srv-url: ${default-url}

 # -------------------- SIDECAR MODULE ------------------------
 - name: user-roles-db-deployer
 # ------------------------------------------------------------
   type: hdb
   path: gen/db  
   parameters:
     buildpack: nodejs_buildpack
   requires:
    # 'hana' and 'xsuaa' resources extracted from CAP configuration
    - name: user-roles-db


resources:
 # services extracted from CAP configuration
 # 'service-plan' can be configured via 'cds.requires.<name>.vcap.plan'
# ------------------------------------------------------------
 - name: user-roles-db
# ------------------------------------------------------------
   type: com.sap.xs.hdi-container
   parameters:
     service: hanatrial  # or 'hanatrial' on trial landscapes
     service-plan: hdi-shared
   properties:
     hdi-service-name: ${service-name}

 - name: user-roles_destination
   type: org.cloudfoundry.managed-service
   parameters:
     service-plan: lite
     service: destination
     config:
       HTML5Runtime_enabled: true
       version: 1.0.0     

 - name: user-roles_uaa
   type: org.cloudfoundry.managed-service
   parameters:
     path: ./xs-security.json
     service-plan: application
     service: xsuaa  
```

xs-security.jsonは以下のようになっています。

```json
{
  "xsappname": "user-roles",
  "tenant-mode": "dedicated",
  "scopes": [],
  "attributes": [],
  "role-templates": []
}
```

### 2.5. デプロイ
ビルド、デプロイして、BTPに登録されたURLを実行してみます。

```
mbt build
cf deploy mta_archives/user-roles_1.0.0.mtar
```
アクションを実行するにはPOSTリクエストを投げる必要があるので、VS Codeの[REST Client](https://qiita.com/toshi0607/items/c4440d3fbfa72eac840c)を使ってテストします。任意の場所に`test.http`というファイルを作って以下のリクエストを送信してみます。

```:test.http
POST https://<route>.cfapps.eu10.hana.ondemand.com/user/loadUsers
Content-Type: application/json
```
以下のレスポンスが返ってくれば成功です。

```
{
  "@odata.context": "$metadata#Edm.String",
  "value": "3 records have been inserted."
}
```
`/user/Users`を開くと、以下のようにユーザデータが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/bbac880a-f108-fce7-a478-2ec5d2b4454c.png)


### 2.6. ローカルでテスト
以下は、ローカル実行でもDestinationにアクセスできるようにするための設定です。
まず、設定なしにlocalhostに向けて以下のリクエストを実行してみます。

```
POST http://localhost:4004/user/loadUsers
Content-Type: application/json
```
すると、以下のエラーになります。

```
{
  "error": {
    "code": "500",
    "message": "Failed to load destination."
  }
}
```

**設定**
デプロイしたサービスの環境変数から、`VCAP_SERVICES`の部分をコピーしてプロジェクト直下の`default-env.json`に貼り付けます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/209dcfd2-00d1-46bd-ecb7-866c2144a778.png)
以下は"hanatrial", "destination", "xsuaa"の各セクションを折りたたんだ状態です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/269e4114-11ae-ba56-bf2e-d49af2017756.png)
このあと、先ほどと同じリクエストを実行してみましょう。以下のような結果が返ってくれば成功です。

```
{
  "@odata.context": "$metadata#Edm.String",
  "value": "3 records have been inserted."
}
```
## 3. Job Scheduling ServiceからCAPのサービスを呼ぶ
ここでは、Job Scheduling Serviceのトライアル版であるliteプランを使います。

### 3.1. サービスインスタンスの作成
以下のコマンドでJob Scheduling Serviceのインスタンスを登録します。
※以下の-c以降の指定方法は、Windowsの場合です。

```
cf create-service jobscheduler lite job-scheduler -c '{\"enable-xsuaa-support\": true}'
```
※はじめはBTPのコックピットから登録しようとしたのですが、以下のエラーが出たためCLIで登録しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/b43eb2ae-6964-ff1d-5756-7a86e26c2903.png)

### 3.2. CAPのサービスにJob Scheduling Serviceのインスタンスをバインド
CAPのmta.yamlを以下のように変更します。

- resourcesセクションに`job-scheduler`を追加

```yaml
resources:
#...
 - name: job-scheduler
   type: org.cloudfoundry.existing-service
```

- `user-roles-srv`に`job-scheduler`をバインド

```yaml
modules:
 # --------------------- SERVER MODULE ------------------------
 - name: user-roles-srv
 # ------------------------------------------------------------
   type: nodejs
   path: gen/srv
   parameters:
     buildpack: nodejs_buildpack
   requires:
    # Resources extracted from CAP configuration
    - name: user-roles-db
    - name: user-roles_destination
    - name: user-roles_uaa
    - name: job-scheduler #<--追加
   provides:
    - name: srv-api      # required by consumers of CAP services (e.g. approuter)
      properties:
        srv-url: ${default-url}
```
以上の設定をしたら、CAPプロジェクトを再度ビルド、デプロイします。

### 3.3. ジョブの設定
Job Scheduling Serviceのインスタンスからダッシュボードを開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/7a143924-2d68-b798-17c4-f7252fb5e525.png)

- ジョブの登録
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/2e94a2bd-cfed-6bd9-664e-601466cb7cc1.png)

|項目  |設定値  |
|---|---|
|Name  |任意  |
|Description  |任意  |
|Target Application  |user-role-srv ※バインドしたアプリケーションがドロップダウンで選べる  |
|Action  |user-role-srvのURL  |
|HTTP Method  |POST  |
|Activate Job  |YES  |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/287af850-6c8a-d292-6d80-e9347d12d3ff.png)

- スケジュールの登録

登録したジョブをクリックして中に入ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/76ba29e4-b045-d331-294a-eb532130e95b.png)
スケジュールを登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/caa0fa2b-f459-68ff-22f0-cd1deabc00be.png)
スケジュールのパターンは以下が選べます。今回は`One Time`を選択します。その他のパターンについては[ドキュメント](https://help.sap.com/viewer/07b57c2f4b944bcd8470d024723a1631/Cloud/en-US/9cf8c14da0144c84aac628dc56b00ffd.html?q=Cron)をご参照ください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/97054f97-3fcd-b96e-1aca-7ccecd64584a.png)
以下のようにスケジュールを指定しました。Valueには`now`や`10 hours from now`など、[英語の表現](https://help.sap.com/viewer/07b57c2f4b944bcd8470d024723a1631/Cloud/en-US/54615f087cca45c48f81ce4967c6f7f3.html#loioa323f2d365904499a83a1b60f473bb78)を指定できます。Dataには、POSTリクエストで渡すデータを指定します。今回、CAPのアクションはパラメータを必要としませんが、ヘッダの`Content-Type`は必須なので、ヘッダをつけるために空のデータ`{}`を渡します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/37335e81-6e39-bfd1-bfda-a5c54d6a7606.png)
`now`を指定したので、スケジュールを保存するとすぐに実行されます。"Run Logs"のメニューから実行結果を確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/5a727d61-9e07-70e2-da3c-9402cc0ef5e8.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d2c8fd74-c122-1bbc-f542-f9006b11a79d.png)

※時刻を指定する場合、**UTC**で指定する必要があります。以下は日本時間の16:30に毎日実行したい場合の指定方法です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/1d98e0f5-ec01-93cc-cb8c-0a8fd5eb801b.png)

### （補足）認証が必要なアプリケーションにアクセスする場合
Job Scheduling Serviceから認証で守られたアプリケーションを呼ぶためには、アプリケーションにバインドされたXSUAAの設定で、以下のようにJob Scheduling Serviceからの呼び出しを許可する必要があります。

```json
{
    "xsappname": "<app name>",
    "scopes": [{
        "name": "$XSAPPNAME.Jobs",
        "description": "SAP Job Scheduling
                                service Scope",
        "grant-as-authority-to-apps": [
            "$XSSERVICENAME(<jobscheduler instance name>)"
            ]
    }]
}
```
参考：[Secure Acces](https://help.sap.com/viewer/07b57c2f4b944bcd8470d024723a1631/Cloud/en-US/745ca502face47af9adb546a916ce1e8.html)

トライアル版で上記の設定を試したところ、job-scheduler（Job Scheduling Serviceのサービスインスタンス）に割り当てられたxsuaaサービスが見つからないというエラーになりました。トライアル版ではOAuth2がサポートされていないので、この設定はできないのかもしれません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/a8e315af-68e9-e575-f3f9-db95baaac802.png)

# まとめ
このブログでは、「ログインしていないユーザの権限をチェック」するため、以下の仕組みを実装しました。

- Authorization and Trust Management ServiceのAPIを使い、BTPのサブアカウントに登録されているユーザの一覧、ロールコレクション、およびロールを取得する
- 上記の処理を、Job Scheduling Serviceを使用して日次のジョブで実行する

#参考
##Authorization and Trust Management Serviceについて
- [Access UAA Admin APIs](https://help.sap.com/viewer/65de2977205c403bbc107264b8eccf4b/Cloud/en-US/ebc9113a520e495ea5fb759b9a7929f2.html)

##SAP Cloud SDKのGeneric HTTP Clientについて
- [Document](https://sap.github.io/cloud-sdk/docs/js/features/connectivity/generic-http-client)
- [Use CloudSDK for http request in NodeJS on SCP CF](https://blogs.sap.com/2020/10/23/use-cloudsdk-for-http-request-in-nodejs-on-scp-cf/)

##Job Scheduling Serviceについて
- [Create a Service Instance Using CF CLI](https://help.sap.com/viewer/07b57c2f4b944bcd8470d024723a1631/Cloud/en-US/cb56f9efa8814a3f8276076614e8373a.html)
- [Using Job Scheduler in SAP Cloud Platform [0]: Intro and Prep](https://blogs.sap.com/2019/11/25/using-job-scheduler-in-sap-cloud-platform-0-intro-and-prep/)
- [Using Job Scheduler in SAP Cloud Platform [1]: First simple scenario](https://blogs.sap.com/2019/11/26/using-job-scheduler-in-sap-cloud-platform-1-first-simple-scenario/)