今回は、「IRIS for Health Contest」に応募するために、どのような技術を使ってアプリケーションを開発していたのか、その詳細を紹介したいと思います。
- OpenAPI仕様からのREST API生成
- APIとWebページを保護する役割ベースのアクセス(RBAC)
- InterSystems FHIR サーバー
内容
- アプリケーション概要
- OpenAPI仕様からのREST API生成
- APIとWebページを保護する役割ベースのアクセス(RBAC)
* REST APIの安全性確保
* Webページの安全性確保
* リソースと役割の作成
- InterSystems FHIR サーバー
アプリケーション概要
まず、それらの技術に支えられたアプリケーションを簡単に紹介します。
このアプリケーションは、妊娠中の女性が簡単に症状を報告できるように設計されています。このアプリケーションはレスポンシブであるため、モバイルデバイスを使用して症状を簡単に報告することができます。このような症状は、FHIR Observation リソース のInterSystems FHIR サーバーを使って記録されます。.
患者と医師は、通常のリレーショナル・テーブルを使用し、患者 と 医師 のFHIRリソースのIDを参照して連携しています。そのため、医師は患者がどのような症状を訴えているのかを確認することもでき、万が一の事態に迅速に対応することができます。
アプリケーションは、IRIS リソース と 役割を使用して患者/医師を識別し、アクセス権限を制御します。
FHIRリソースは、アプリケーションのフロントエンドで利用可能なREST APIによってアクセスされます。IRIS Interoperability Credentialsに格納されたAPI KEYを使用して、FHIRサーバーへのHTTPS接続が確立されます。
アプリケーションのWebリソースは、IRIS Web Gatewayで扱われます。
OpenAPI仕様からのREST API生成
IRISプラットフォームでは、手動またはOpenAPI仕様を介してRESTインターフェースを定義することができます。
OpenAPIを使用することは、デザインファーストのアプローチ、容易な変更追跡、容易な文書化、強力なモデル、設計、モッキング、テストなどのための多くのツールなど、多くの利点があります。
そこで、IRIS REST Servicesを使って、OpenAPI仕様からコードを生成する方法に焦点を当てます。
まず、OpenAPIを使ってAPIを設計する必要があります。今回は、VS Codeの拡張機能であるOpenAPI (Swagger) Editorを使用しました。これは、エンドポイントやその他のOpenAPIリソースをVS Codeで直接作成するのに役立ちます。
OpenAPI仕様でAPIを設計したら、それをJSONファイルに保存する必要があります。ここでは、このファイルにAPIを保存しています。
これで、IRIS REST Services を使用して API 用のコードを生成する準備が整いました。これには3つのオプションがあります。
- REST service/api/mgmntを使用
^%REST
ルーチンを使用%REST.API
classを使用
この記事では、最後の1つである %REST.API
クラスを使用することにします。それでは、IRIS端末を開いて、以下のコードを実行してください。
Set applicationName = "dc.apps.pregsymptracker.restapi"
Set swagger = "/irisrun/repo/src/openapi/pregnancy-symptoms-tracker.json"
ZW ##class(%REST.API).CreateApplication(applicationName, swagger, , .newApplication, .internalError)
ZW newApplication
ZW internalError
OpenApi仕様のJSONファイルの場所は、swagger
パラメータで設定されます。
applicationName
パラメータは、IRIS REST Servicesが生成されたコードを格納するパッケージ名です。
3つのクラスが生成されます。
-
spec.cls
: OpenAPI仕様のための単なるコンテナです。このクラスは編集しないでください。 -
impl.cls
: メソッドの実装を含むメインクラスです。このクラスは、APIロジックを開発するために、自分で編集することを意図しています。ヒント: OpenAPIのメソッドの名前は、必ずIRIS拡張属性operationId
を使って、 ここで のように定義します。この属性を使用しない場合、IRIS はランダムな名前のメソッドを作成します。 -
disp.cls
: ディスパッチクラスは、IRIS で REST API を公開するために Web アプリケーションをバインドするクラスです。ヒント: このクラスを表示するには、生成されたアイテムを表示していることを確認します。このクラスを編集することもできますが、あまりお勧めできませんが、IRIS に任せてください。
最後の2つのパラメータ、 newApplication
と internalError
は出力パラメータで、それぞれ API が作成または更新されたかどうか、そして OpenAPI のパースやクラスの生成時に発生した可能性のあるエラーを返すものです。この情報をチェックするために書き出すだけです。
OpenAPI の仕様を更新した場合、コードを更新するために CreateApplication
メソッドを再度実行する必要があります。 impl
クラスに実装した以前のロジックコードはそのまま残し、IRIS REST Service が修正を行った箇所にはコメントを追加します。
APIとWebページを保護する役割ベースのアクセス(RBAC)
前述したように、このアプリケーションには、患者と医師の2種類のユーザーが存在します。そこで、この2種類のユーザー間でアプリケーションのリソースに対するアクセスルールを設計するために、リソースと役割を使用しました。
ユーザーは役割を与えられ、役割にはリソースへの権限があり、リソースは例えばREST APIのようなシステムリソースにアクセスするために必要であるべきです。
REST APIの安全性確保
IRIS REST Serviceでは、OpenAPIのIRIS拡張子である x-ISC_RequiredResource
属性によって、サービスにアクセスするために必要な権限を指定することができました。この属性は、API全体、または特定のエンドポイントに対して、次のように指定することができます:
"paths": {
"/symptom": {
"post": {
"operationId": "PostSymptom",
"x-ISC_RequiredResource": ["AppSymptoms:write"],
"description": "患者さんが自分の症状を報告するために使用する",
…
"/doctor/patients": {
"get": {
"operationId": "GetDoctorPatientsList",
"x-ISC_RequiredResource": ["AppAccessDoctorPatients:read"],
"description": "現在ログインしている医師の患者を取得する",
…
OpenAPI仕様でAPIクラスを生成した後 - 前に説明したように , IRIS REST Serviceがx-ISC_RequiredResource
制約をdisp
クラスでどのように実装しているかを見ることができます:
ClassMethod PostSymptom() As %Status
{
Try {
Set authorized=0
Do {
If '$system.Security.Check("AppSymptoms","write") Quit
Set authorized=1
} While 0
If 'authorized Do ##class(%REST.Impl).%ReportRESTError(..#HTTP403FORBIDDEN,$$$ERROR($$$RESTResource)) Quit
…
} Catch (ex) {
Do ##class(%REST.Impl).%ReportRESTError(..#HTTP500INTERNALSERVERERROR,ex.AsStatus(),$parameter("dc.apps.pregsymptracker.restapi.impl","ExposeServerExceptions"))
}
Quit $$$OK
}
RBACを使ったAPIの保護方法については、このページをご覧ください。
Webページの安全性確保
このアプリケーションでは、Web アプリケーションを実装するために CSP ページを使用しました。この技術は、現在の SPA に比べて古いと考えられていますが、それでもまだ利点があります。
例えば、ユーザーがページにアクセスするために、特定の役割を持つことを要求することができます。つまり、REST API のエンドポイントを保護することをまとめると、先に述べたように、アプリケーションに追加のセキュリティ・レイヤーを定義することができるのです。
ユーザがシステムにログインするとき、そのユーザに役割が割り当てられている場合、IRISはそのユーザに役割を割り当てます。このような役割は、CSP コンテキストで $ROLE
コンテキスト変数を通してアクセスすることができ、ユーザに割り当てられた特定の役割を要求するために使用することができます。
<!-- patient.csp -->
<script language="cache" method="OnPreHTTP" arguments="" returntype="%Boolean">
Do ##class(dc.apps.pregsymptracker.util.Util).AssertRole("AppPatient")
Return 1
</script>
<!-- doctor.csp -->
<script language="cache" method="OnPreHTTP" arguments="" returntype="%Boolean">
Do ##class(dc.apps.pregsymptracker.util.Util).AssertRole("AppDoctor")
Return 1
</script>
ClassMethod AssertRole(pRole As %String)
{
If ('$Find($ROLES, pRole)){
Set %response.Redirect = "NoPrivilegesPage.csp"
}
}
もし、現在のユーザーが patient.csp
ページを評価するときに AppPatient
役割を持っていない場合、IRIS Web サーバはそのユーザーを NoPrivilegesPage.csp
ページにリダイレクトし、ユーザーにセキュリティ問題を通知するメッセージを表示します。doctor.cpsページも同様ですが、今度は
AppDoctor` 役割が必要です。
この例では、AppPatient
とAppDoctor
の2つの役割を持つことができます。つまり、そのユーザーは患者であると同時に医師でもあり、両方のページにアクセスすることができるのです。
リソースと役割の作成
IRISポータルでリソース, 役割 を作成し、ユーザー に割り当てることができます - これは簡単なことです。しかし、ここではプログラムでそれらを作成する方法を紹介したいと思います:
ClassMethod CreateResources()
{
Do ..Log("アプリケーションリソースを作成する...")
Set ns = $NAMESPACE
Try {
ZN "%SYS"
Do $CLASSMETHOD("Security.Resources", "Delete", "AppSymptoms")
Do $CLASSMETHOD("Security.Resources", "Delete", "AppAccessDoctorPatients")
$$$TOE(st, $CLASSMETHOD("Security.Resources", "Create", "AppSymptoms", "患者の症状", "RWU", ""))
$$$TOE(st, $CLASSMETHOD("Security.Resources", "Create", "AppAccessDoctorPatients", "患者のアクセス権", "RWU", ""))
} Catch(e) {
ZN ns
Throw e
}
ZN ns
}
ClassMethod CreateRoles()
{
Do ..Log("アプリケーション役割を作成する...")
Set ns = $NAMESPACE
Try {
ZN "%SYS"
Do $CLASSMETHOD("Security.Roles", "Delete", "AppPatient")
Do $CLASSMETHOD("Security.Roles", "Delete", "AppDoctor")
$$$TOE(st, $CLASSMETHOD("Security.Roles", "Create", "AppPatient", "アプリケーション での患者の役割", "AppSymptoms:RWU", ""))
$$$TOE(st, $CLASSMETHOD("Security.Roles", "Create", "AppDoctor", "アプリケーション での医師の役割", "AppSymptoms:RWU,AppAccessDoctorPatients:RWU", ""))
} Catch(e) {
ZN ns
Throw e
}
ZN ns
}
ClassMethod CreateUsers()
{
Do ##class(dc.apps.pregsymptracker.util.Setup).Log("サンプルユーザーを作成する...")
//ある患者
&SQL(drop user MarySmith)
&SQL(create user MarySmith identified by 'marysmith')
&SQL(grant %DB_IRISAPP, %DB_IRISAPPSECONDARY, AppPatient to MarySmith)
&SQL(grant select on schema dc_apps_pregsymptracker_data to MarySmith)
//他患者
&SQL(drop user SuzieMartinez)
&SQL(create user SuzieMartinez identified by 'suziemartinez')
&SQL(grant %DB_IRISAPP, %DB_IRISAPPSECONDARY, AppPatient to SuzieMartinez)
&SQL(grant select on schema dc_apps_pregsymptracker_data to SuzieMartinez)
//ある医師
&SQL(drop user PeterMorgan)
&SQL(create user PeterMorgan identified by 'petermorgan')
&SQL(grant %DB_IRISAPP, %DB_IRISAPPSECONDARY, AppDoctor to PeterMorgan)
&SQL(grant select on schema dc_apps_pregsymptracker_data to PeterMorgan)
// 患者である医師
&SQL(drop user AnneJackson)
&SQL(create user AnneJackson identified by 'annejackson')
&SQL(grant %DB_IRISAPP, %DB_IRISAPPSECONDARY, AppDoctor, AppPatient to AnneJackson)
&SQL(grant select on schema dc_apps_pregsymptracker_data to AnneJackson)
}
InterSystems FHIR サーバー
InterSystems FHIR サーバー は、IRIS for Health と同じように、FHIR リソースへのアクセスをクラウド上で提供するサービスです。
FHIR ServerではOAuth2が可能で、SMART on FHIR JavaScript Library などのライブラリを使ってアプリケーションから直接FHIRリソースにアクセスできますが、このアプリケーションでは、FHIR Serverをメインデータリポジトリとして使用しながら、IRISにローカルに保存されているメタデータで制御するハイブリッドアプローチを選択しました。
そこで、バックエンドがFHIR ServerでFHIRトランザクションを実行するために使用するFHIRクライアントを作成しました。このクライアントは、サーバーが生成したAPI KEYを使用して、FHIR ServerへのHTTPSコールを実行するために、%Net.HttpRequest
を使用して実装されています。
これはHTTPクライアントのコードで、 `` を使って基本的なHTTP動詞を実装しています。
Class dc.apps.pregsymptracker.restapi.HTTPClient Extends %RegisteredObject
{
Property Request As %Net.HttpRequest;
Property Server As %String;
Property Port As %String;
Property UseHTTPS As %Boolean;
Property SSLConfig As %String;
Property APIKeyCred As %String;
Method CreateRequest()
{
Set ..Request = ##class(%Net.HttpRequest).%New()
Set ..Request.Server = ..Server
Set ..Request.Port = ..Port
Set ..Request.Https = ..UseHTTPS
If (..UseHTTPS) {
Do ..Request.SSLConfigurationSet(..SSLConfig)
}
}
Method SetHeaders(headers As %DynamicObject)
{
Set headersIt = headers.%GetIterator()
While (headersIt.%GetNext(.headerName, .headerValue)) {
Do ..Request.SetHeader(headerName, headerValue)
}
}
Method GetApiKeyFromEnsCredentials() As %String
{
Set apiKeyCred = ..APIKeyCred
$$$TOE(st, ##class(Ens.Config.Credentials).GetCredentialsObj(.apiKeyCredObj, "", "Ens.Config.Credentials", apiKeyCred))
Return apiKeyCredObj.Password
}
Method HTTPGet(pPath As %String) As %Net.HttpResponse
{
Do ..CreateRequest()
$$$TOE(st, ..Request.Get(pPath))
Set response = ..Request.HttpResponse
Return response
}
Method HTTPPost(pPath As %String, pBody As %DynamicObject) As %Net.HttpResponse
{
Do ..CreateRequest()
Do ..Request.EntityBody.Clear()
Do ..Request.EntityBody.Write(pBody.%ToJSON())
$$$TOE(st, ..Request.Post(pPath))
Set response = ..Request.HttpResponse
Return response
}
Method HTTPPut(pPath As %String, pBody As %DynamicObject) As %Net.HttpResponse
{
Do ..CreateRequest()
Do ..Request.EntityBody.Clear()
Do ..Request.EntityBody.Write(pBody.%ToJSON())
$$$TOE(st, ..Request.Put(pPath))
Set response = ..Request.HttpResponse
Return response
}
Method HTTPDelete(pPath As %String) As %Net.HttpResponse
{
Do ..CreateRequest()
$$$TOE(st, ..Request.Delete(pPath))
Set response = ..Request.HttpResponse
Return response
}
}
そしてこれがFHIRクライアントのコードで、HTTPクライアントを拡張し、CreateRequestメソッドをオーバーライドしてHTTPコールにFHIRサーバーのAPIキーを自動的に付加しています。
Class dc.apps.pregsymptracker.restapi.FHIRaaSClient Extends dc.apps.pregsymptracker.restapi.HTTPClient
{
Method CreateRequest()
{
Do ##super()
Do ..SetHeaders({
"x-api-key" : (..GetApiKeyFromEnsCredentials())
})
}
}