#はじめに
この記事は chillSAP 夏の自由研究2021 の記事として執筆しています。
私は最近、SAP Workflow Serviceの個人的PoCと称してBTPのWorkflowにからめた検証を行っています。その中で、以下のような要件が出てきました。
- ユーザが画面で指定した承認者が、適切な権限を持っているかチェックしたい
- 画面(UI5アプリ)のバックエンドであるCAPの中で上記のチェックを行う
「ログインユーザの権限」を見るのであれば、CAPの中でスコープのチェックができます。しかし、承認者として設定されたユーザはログインユーザではないので、権限チェックは簡単にはできません。
#BTPにおける権限の仕組み
BTPにおける権限の最小単位はスコープです。スコープはデータに対する読み込み、書き込みなどの権限を表します。一つまたは複数のスコープがロールテンプレートを介してロールに割り当てられます。ロールはロールコレクションを介してユーザに割り当てられます。
よって、「ユーザが特定のスコープを持っているか」をチェックする場合、ユーザ⇒ロールコレクション⇒ロール⇒スコープの順に展開する必要があります。
図:User Management in SAP HANA Cloud Cockpitより引用
#考えた権限チェックの仕組み
以下の図は、PoCで実現しようとしてる仕組みの全体です。CAP1はUI5アプリから呼ばれ、ワークフローを起動したり、承認、却下などのタスクを実行します。CAP2は、ユーザやロールコレクションなどのマスタデータを持ち、ユーザの権限チェックのためのAPIを提供します。CAP2を中心とする、黄色い枠で囲った部分が今回の記事で説明する範囲です。
CAP2では、Authorization and Trust Management ServiceのAPIを使い、BTPのサブアカウントに登録されているユーザの一覧、ロールコレクション、およびロールを取得してHANA DBに格納します。この処理を、Job Scheduling Serviceを使用して日次のジョブで実行します。
権限チェックでは、上記のマスタをもとに承認者に設定されたユーザが持っているスコープを取得します(下図)。ユーザやロールコレクションといったマスタの更新頻度に比べ、ユーザへのロール割り当て更新の頻度は高い(日中にも行う可能性がある)という想定で、ユーザに割り当たったロールコレクションを取得するところだけはリアルタイムに行うようにしました。
##技術的なポイント
技術的なポイント(私が未経験だったこと)は、以下の2点です。
- CAPからAuthorization and Trust Management ServiceのAPI(REST API)を呼ぶ
- Job Scheduling ServiceでCAPのアクションを呼ぶ
#実装方法
以下で実装方法について説明していきます。ステップは大きく分けて3つです。
- Authorization and Trust Management ServiceのDestination登録
- CAPサービスの実装
- 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
アプリケーションにバインドする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
|
##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を使うのがポイントです。
参考:Use CloudSDK for http request in NodeJS on SCP CF
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のサービスインスタンスをバインドしています。
## 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は以下のようになっています。
{
"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を使ってテストします。任意の場所に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
を開くと、以下のようにユーザデータが表示されます。
###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
に貼り付けます。
以下は"hanatrial", "destination", "xsuaa"の各セクションを折りたたんだ状態です。
このあと、先ほどと同じリクエストを実行してみましょう。以下のような結果が返ってくれば成功です。
{
"@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で登録しました。
###3.2. CAPのサービスにJob Scheduling Serviceのインスタンスをバインド
CAPのmta.yamlを以下のように変更します。
- resourcesセクションに
job-scheduler
を追加
resources:
#...
- name: job-scheduler
type: org.cloudfoundry.existing-service
-
user-roles-srv
にjob-scheduler
をバインド
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のインスタンスからダッシュボードを開きます。
項目 | 設定値 |
---|---|
Name | 任意 |
Description | 任意 |
Target Application | user-role-srv ※バインドしたアプリケーションがドロップダウンで選べる |
Action | user-role-srvのURL |
HTTP Method | POST |
Activate Job | YES |
- スケジュールの登録
登録したジョブをクリックして中に入ります。
スケジュールを登録します。
スケジュールのパターンは以下が選べます。今回はOne Time
を選択します。その他のパターンについてはドキュメントをご参照ください。
以下のようにスケジュールを指定しました。Valueにはnow
や10 hours from now
など、英語の表現を指定できます。Dataには、POSTリクエストで渡すデータを指定します。今回、CAPのアクションはパラメータを必要としませんが、ヘッダのContent-Type
は必須なので、ヘッダをつけるために空のデータ{}
を渡します。
now
を指定したので、スケジュールを保存するとすぐに実行されます。"Run Logs"のメニューから実行結果を確認できます。
※時刻を指定する場合、UTCで指定する必要があります。以下は日本時間の16:30に毎日実行したい場合の指定方法です。
###(補足)認証が必要なアプリケーションにアクセスする場合
Job Scheduling Serviceから認証で守られたアプリケーションを呼ぶためには、アプリケーションにバインドされたXSUAAの設定で、以下のようにJob Scheduling Serviceからの呼び出しを許可する必要があります。
{
"xsappname": "<app name>",
"scopes": [{
"name": "$XSAPPNAME.Jobs",
"description": "SAP Job Scheduling
service Scope",
"grant-as-authority-to-apps": [
"$XSSERVICENAME(<jobscheduler instance name>)"
]
}]
}
参考:Secure Acces
トライアル版で上記の設定を試したところ、job-scheduler(Job Scheduling Serviceのサービスインスタンス)に割り当てられたxsuaaサービスが見つからないというエラーになりました。トライアル版ではOAuth2がサポートされていないので、この設定はできないのかもしれません。
#まとめ
このブログでは、「ログインしていないユーザの権限をチェック」するため、以下の仕組みを実装しました。
- Authorization and Trust Management ServiceのAPIを使い、BTPのサブアカウントに登録されているユーザの一覧、ロールコレクション、およびロールを取得する
- 上記の処理を、Job Scheduling Serviceを使用して日次のジョブで実行する
#参考
##Authorization and Trust Management Serviceについて
##SAP Cloud SDKのGeneric HTTP Clientについて
##Job Scheduling Serviceについて