記事 Shintaro Kaminaka · 11月 3, 2021 15m read

開発者の皆さん、こんにちは。

この記事は、FHIRの関連技術として、FHIRプロファイル作成ツールであるSUSHIの使い方を紹介するシリーズの第2弾です。パート2である今回まで半年の期間が経ってしまいました。

前回のパート1では、FHIRとは?FHIRプロファイルとは?FHIR Shorthandとは?そしてSUSHIとはどのようなツールなのか?どのような物を作成できるのか?について、サンプルの成果物のスクリーンショットを交えながら説明しました。

今回の記事では、SUSHIで作成したプロファイルの実際の活用例として、SUSHIを使ってPatientリソースに Extension を追加し、さらにそのExtensionの項目に対する新しい SearchParameter を定義し、IRIS for HealthのFHIR Repositoyで新しいSearchParameterが使えるようになるまで、をご紹介します。

SUSHIのアップグレード

いきなり本筋とそれて恐縮ですが、SUSHIを久しぶりに触る私のような方はSUSHIのアップグレードを行いましょう。 この半年の間もSUSHIは精力的な機能Enhancementが行われており、8月にはversion 2.0.0がリリースされています。この記事の執筆段階の最新バージョンは SUSHI 2.1.1 でした。

このリンク先でも紹介されている通り、アップグレードはインストール同様以下のコマンドです。

$ npm install -g fsh-sushi

sushi -versionを実行すればバージョンが確認できます。 同様に、SUSHIで生成されたProfileをベースに、実装ガイドのHTMLファイル群を作成してくれるIG Publisherツールも、_updatePublisher コマンドを実行してアップグレードすることができます。

FISHファイルの作成

まずは、前回同様 sushi --init コマンドを使って、プロジェクトを作成します。この記事ではテンプレートで生成される、patient.fsh ファイルを修正していきます。

今回は、出身都道府県を表現するstring型の birthPlace のExtensionを追加し、さらにそのbirthPlaceに対するSearchParameterも定義することで、その患者の出身都道府県で検索できるような拡張を行います!

Extensionを追加する

まず、Extensionを追加するために以下の定義を追加します。

US CoreやJP Coreのように、通常はAddress型を使うことが多いですが、ここでは単純にstring型にしています。

Extension: BirthPlace
Id: birthPlace
Title: "出身地"
Description: "生まれた場所をstring型で表現する"
* ^url = "http://isc-demo/fhir/StructureDefinition/patient-birthPlace" 
* value[x] only string 

各項目は以下のようにExtensionのStructureDefinitionに対応しています。項目によっては複数の箇所に設定されます。ベースとなるfhirのバージョンや、このExtension自体のバージョンなどの情報は、sushi-config.ymlファイルから取得されているものもあります。

SUSHIの項目対応するStructureDefinitionの項目
Extensionsname
Idid
Titletitle/differencial.element[id=Extension].short
Desctiptiondescription/differencial.element[id=Extension].definition
^urlurl//differencial.element[id=Extension.url].fixedUri
value[x]differencial.element[id=Extension.value[x]].type.code

実際に生成されたExtensionのStructureDefinitionです。 手書きでこれを1から作るのは大変ですが、SUSHIを使えば比較的簡単です。

{
  "resourceType": "StructureDefinition",
  "id": "birthPlace",
  "url": "http://isc-demo/fhir/StructureDefinition/patient-birthPlace",
  "version": "0.1.0",
  "name": "BirthPlace",
  "title": "出身地",
  "status": "active",
  "description": "生まれた場所をstring型で表現する",
  "fhirVersion": "4.0.1",
  "mapping": [
    {
      "identity": "rim",
      "uri": "http://hl7.org/v3",
      "name": "RIM Mapping"
    }
  ],
  "kind": "complex-type",
  "abstract": false,
  "context": [
    {
      "type": "element",
      "expression": "Element"
    }
  ],
  "type": "Extension",
  "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Extension",
  "derivation": "constraint",
  "differential": {
    "element": [
      {
        "id": "Extension",
        "path": "Extension",
        "short": "出身地",
        "definition": "生まれた場所をstring型で表現する"
      },
      {
        "id": "Extension.extension",
        "path": "Extension.extension",
        "max": "0"
      },
      {
        "id": "Extension.url",
        "path": "Extension.url",
        "fixedUri": "http://isc-demo/fhir/StructureDefinition/patient-birthPlace"
      },
      {
        "id": "Extension.value[x]",
        "path": "Extension.value[x]",
        "type": [
          {
            "code": "string"
          }
        ]
      }
    ]
  }
}

このExtensionで追加したPatientリソースへのExtensionデータは、実際はこのようなデータになります。


  "extension": [
    {
      "url": "http://isc-demo/fhir/StructureDefinition/patient-birthPlace",
      "valueString": "鹿児島"
    }
  ],

SearchParamterを追加する

次は、先ほど追加したExtensionの項目をキーにして、リソースを検索できるように、SearchParamter を追加します。FHIRの場合、各リソースには構造化された要素(エレメント)が定義されていますが、 そのすべての要素で検索ができるわけではなく、SearchParamterに定義された項目(≒要素)でのみ検索することができます 。ここがSQLのテーブルとは少し異なる点ですね。

SearchParamter名は要素名とは別に定義されており、Patientリソースで言えば、genderのように要素名=SearchParameter名で一致するものもあれば、要素名=address.country -> SearchParamter名=address-country のように構造化された要素では一致しないものもあります。

Extensionに追加される項目は当然ながら(何がはいってくるかわからないので)デフォルトではSearchParameterにはならないわけですが、あえてExtensionを定義して格納する方針を定めるようなExtensionは重要な項目であることも多いですよね。

以下のようなSearchParameter定義を生成するための内容をpatient.fshファイルに追加します。

Instance: BirthPlaceSearchParameter
InstanceOf: SearchParameter
Usage: #definition
* url = "http://isc-demo/fhir/SearchParameter/patient-birthPlace"
* version = "0.0.1"
* name = "birthPlace"
* status = #active
* description = "出身地検索のパラメータ"
* code = #birthPlace
* base = #Patient
* type = #string
* expression = "Patient.extension.where(url='http://isc-demo/fhir/StructureDefinition/patient-birthPlace').value"
* comparator = #eq

SearchParameterで生成されるStructureDefinitionはこちらです。 比較的シンプルな定義なので、上記SUSHIの情報とのマッピングは理解しやすいと思います。

{
  "resourceType": "SearchParameter",
  "id": "BirthPlaceSearchParameter",
  "url": "http://isc-demo/fhir/SearchParameter/patient-birthPlace",
  "version": "0.0.1",
  "name": "birthPlace",
  "status": "active",
  "description": "出身地検索のパラメータ",
  "code": "birthPlace",
  "base": [
    "Patient"
  ],
  "type": "string",
  "expression": "Patient.extension.where(url='http://isc-demo/fhir/StructureDefinition/patient-birthPlace').value",
  "comparator": [
    "eq"
  ]
}

特にSearchParameterの定義として、重要になるのは expression の項目と comparator になります。 expressionには対象となるSearchParameterへの FHIRPath 式を記述します。FHIRPathも詳しく説明すると長くなるので興味のある方はこちらの公式ページをご覧ください。

今回の定義で使っている

Patient.extension.where(url='http://isc-demo/fhir/StructureDefinition/patient-birthPlace').value"

こちらの式は、PatientリソースのJson構造に従って、階層順にPatient.extensionと指定し、複数存在する可能性があるExtensionの中から、url=(省略) である今回のExtensionを絞り込み、そのvalueを指定しています。

comparatorはどのような比較式が使えるかを指定します。詳細はこちらをご覧下さい。

Patientに作成したExtension定義を追加する

もう一つ大事な変更があります。Patientリソースでこの作成した BirthPlace Extensionを追加することです。元々自動生成されたPatientリソースのProfile定義MyProfileを以下のように変更します。name要素のCardinalityの変更はコメントアウトしました。

Profile: MyPatient
Parent: Patient
Description: "An example profile of the Patient resource."
//* name 1..* MS
* extension contains BirthPlace named birthPlace 0..1

先ほど追加した"BirthPlace"という名前のExtensionを、Patientリソース内にbirthPlaceという名前でCardinality 0..1 で追加しています。

ついでにテスト用リソースを作成

SUSHIでは、例示用などの目的で使用できるリソースのInstanceを作成することもできます。テストのためにこちらも利用しておきましょう。今定義したExtensionも含めることができます。

Instance: KamiExample
InstanceOf: MyPatient
Description: "Patientリソースのサンプル"
* name.family = "山田"
* extension[BirthPlace].valueString = "鹿児島"

どんなデータができたかは最後のテストでご覧いただきたいと思います。

Let's SUSHI!

FSHファイルの用意ができました!それでは SUSHIコマンドで、fshファイルから各定義ファイルを生成しましょう! sushi コマンドを実行し、以下のように2つのProfile(拡張されたPatientとExtension)、二つのInstance(SearchParameterとサンプルリソース)が生成されたら成功です。

C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject\MyProfileProject>sushi .
info  Running SUSHI v2.1.1 (implements FHIR Shorthand specification v1.2.0)
info  Arguments:
info    C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject\MyProfileProject
info  No output path specified. Output to .
info  Using configuration file: C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject\MyProfileProject\sushi-config.yaml
info  Importing FSH text...
info  Preprocessed 1 documents with 0 aliases.
info  Imported 2 definitions and 2 instances.
info  Checking local cache for hl7.fhir.r4.core#4.0.1...
info  Found hl7.fhir.r4.core#4.0.1 in local cache.
info  Loaded package hl7.fhir.r4.core#4.0.1
(node:27132) Warning: Accessing non-existent property 'INVALID_ALT_NUMBER' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)
(node:27132) Warning: Accessing non-existent property 'INVALID_ALT_NUMBER' of module exports inside circular dependency
info  Converting FSH to FHIR resources...
info  Converted 2 FHIR StructureDefinitions.
info  Converted 2 FHIR instances.
info  Exporting FHIR resources as JSON...
info  Exported 4 FHIR resources as JSON.
info  Assembling Implementation Guide sources...
info  Generated ImplementationGuide-myprofileproject.json
info  Assembled Implementation Guide sources; ready for IG Publisher.

╔════════════════════════ SUSHI RESULTS ══════════════════════════╗
║ ╭───────────────┬──────────────┬──────────────┬───────────────╮ ║
║ │    Profiles   │  Extensions  │   Logicals   │   Resources   │ ║
║ ├───────────────┼──────────────┼──────────────┼───────────────┤ ║
║ │       1       │      1       │      0       │       0       │ ║
║ ╰───────────────┴──────────────┴──────────────┴───────────────╯ ║
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
║ │      ValueSets     │    CodeSystems    │     Instances      │ ║
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
║ │         0          │         0         │         2          │ ║
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
║                                                                 ║
╠═════════════════════════════════════════════════════════════════╣
║ FSHing for compliments? Super job!     0 Errors      0 Warnings ║
╚═════════════════════════════════════════════════════════════════╝

C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject\MyProfileProject>

fsh-generated\resource フォルダには以下のような成果物が作成されました。

ファイル名内容
ImplementationGuide-myprofileproject.json今回の全ての内容を取りまとめたImplemamtionGuide
StructureDefinition-MyPatient.jsonPatientにExtensionを追加したStructureDefinition
StructureDefinition-birthPlace.jsonExtension birthPlaceの定義を含むStructureDefinition
SearchParameter-BirthPlaceSearchParameter.jsonbirthPlace SearchParameterの定義ファイル
Patient-KamiExample.jsonPatientのサンプルインスタンス

IRIS for HealthにFHIR Profileをインポートしてテストしてみる

IRIS for Health のFHIRリポジトリへの適用

前回の記事ではこの後、_updatePublisherを実行してIGファイル群を生成しましたが、今回は、このStuructureDefinitino/SearchParameterファイルをIRIS for HealthのFHIRリポジトリに取り込んで、新しいSearchParameterで検索できるようになるところを見ていきましょう。

FHIR Profileのインポート等について詳細は、こちらの開発者コミュニティ記事 FHIRプロファイルをご覧章ください。 FHIRリポジトリの構築方法などは、こちらの記事も参考になると思います。

インポートの対象となるのは、先ほど生成された5つのファイルのうち、

  • StructureDefinition-MyPatient.json
  • StructureDefinition-birthPlace.json
  • SearchParameter-BirthPlaceSearchParameter.json の3つです。これを別のフォルダにコピーし、さらにパッケージ全体の情報を管理するための package.jsonファイルを用意します。

package.json

{
  "name": "SUSHI Demo",
  "title": "SUSHI Demo",
  "version": "0.0.1",
  "author": {
    "name": "ISC"
  },
  "fhirVersions": [
    "4.0.1"
  ],
  "bundleDependencies": false,
  "date": "20201208205547",
  "dependencies": {
    "hl7.fhir.r4.core": "4.0.1"
  },
  "deprecated": false
}

nameやtitle,author,dateなどの項目は適宜変更して問題ありません。 (注意)各プロファイルを変更してIRISに再インポートする場合は、versionを適切に変更していく(上げていく)必要があります。 (現在のバージョン2021.1ではFHIRリポジトリには、プロファイルを削除する機能がないため、テスト環境で適切に動作確認した上で、本番環境への適用は最小の回数に抑えるなど、本番環境等でプロファイルが増えすぎないように注意をする必要があります。)

IRISの管理ポータルからHealth -> FHIR Configuration -> Package Configurationと進み、Import Packageから上記4ファイルを含むフォルダを選ぶと以下のような画面になります。

image

Importをクリックして、IRISへのインポートを完了します。

次に Server Configuration画面で、新規FHIRリポジトリを作成します。 (既存のFHIRリポジトリへ追加することも可能です。)

image

POSTMANからテストする

先ほどSUSHIで生成された、テスト用リソースをPOSTします。検証のためには他の値のbirthPlaceを含むデータや、そもそもbirthPlaceを含まないPatientリソースなども生成するほうが良いかもしれません。

image

FHIRリポジトリのSearchParameter に正しく birthPlaceが追加されていれば、以下のGETリクエストでこの患者情報を取得できるはずです!

GET http://localhost:52785/csp/healthshare/sushi/fhir/r4/Patient?birthPlace=鹿児島

正しく結果を取得できるようになったでしょうか? 新しいSearchParameterである birthPlaceが正しく追加されていない場合は、GETリクエストの応答の最初に以下の「birthPlaceというパラメータが認識されていません」というエラー情報をが記述されたOperationOutcomeリソースの情報が含まれています。このメッセージがでていないか応答メッセージを確認してみてください。

       {
            "resource": {
                "resourceType": "OperationOutcome",
                "issue": [
                    {
                        "severity": "error",
                        "code": "invalid",
                        "diagnostics": "<HSFHIRErr>ParameterNotSupported",
                        "details": {
                            "text": "Unrecognized parameter 'birthPlace'. 鹿児島"
                        }
                    }
                ]
            },
            "search": {
                "mode": "outcome"
            }
        },

まとめ

SUSHIを使ってFHIRのProfile(StructureDefinition/SearchParameter)を作成し、IRIS for HealthのFHIRリポジトリにインポートして機能を拡張する流れをみていただきました。 今回は、Extensionに追加した項目をSearchParameterに追加しましたが、FHIR標準仕様で存在するが、SearchParameterにはなっていない要素(エレメント)に対して、SearchParameterを追加するということも可能です。

自由度の高いFHIRの開発では、このように機能を拡張することが可能になっていますが、一方ではInteroperabilityを担保するためにどのような拡張を行ったかという情報の共有、つまりImplemantationGuide等の作成も重要になってきます。 このシリーズのPart1,2で見てきたようにSUSHIはその両面をカバーすることができる非常にユニークで強力なオープンソースのツールです。

このようなツールとIRIS for Healthを組み合わせて、新しいFHIRソリューションが構築されることを期待しています。

今回の記事で使用したSUSHIのfshファイルおよび、生成されたStructureDefinition/SearchParameterのサンプルファイルはこちらからダウンロードすることができます。

0
0 1108
記事 Shintaro Kaminaka · 10月 18, 2021 14m read

特報!!

この記事の中でご紹介している、FHIRオペレーションのデモを含め、FHIR PathやFHIRプロファイル対応などの2021.1のFHIR関連新機能をご紹介するウェビナーが、2021年10月21日 12:30~13:00 に開催されます!!ご興味ある方はこちらからご登録ください!!

InterSystems IRIS 開発者向けウェビナーシリーズ

開発者の皆さん、こんにちは。 今日は前回のFHIRリポジトリをカスタマイズしよう!パート1の記事に続き、パート2として、カスタムオペレーションの実装方法をご紹介したいと思います。この記事で紹介している内容のFHIRリポジトリカスタムオペレーションに関するドキュメントマニュアルはこちらになります。 この記事はIRIS for Health 2021.1 をベースに記載しています。バージョンによって実装方法が異なるケースも考えられますので、該当のバージョンのドキュメントをご参照ください。場合によっては新しいバージョンへアップグレードもご検討ください。

FHIRのOperation(オペレーション)とは?

まず、FHIRのOperationについて、簡単にご紹介したいと思います。 HL7 FHIR公式ページのOperationのページには以下のように記載されています。

The RESTful API defines a set of common interactions (read, update, search, etc.) performed on a repository of typed resources. These interactions follow the RESTful paradigm of managing state by Create/Read/Update/Delete actions on a set of identified resources. While this approach solves many use cases, there is some functionality that can be met more efficiently using an RPC-like paradigm, where named operations are performed with inputs and outputs (Execute).

FHIRにおけるデータのアクセスがRESTのCRUDを基本とするのはご存知の通りですが、RPC的なアプローチでより効率的なデータアクセス、データ処理を実現しようというアプローチがFHIRのOperationと言えます。

HL7 FHIR公式ページで規定されているOpeartionはこちら一覧のページに記載されています。 代表的な所では、Patientリソースで指定できる $everything オペレーションや、Observationリソースで指定できる $lastn オペレーションがありますので紹介します。

$everything オペレーション

$everything オペレーションはPatientリソースと組み合わせて使用します。詳細はこちらのページを参照してください。 IRIS for Health のFHIRリポジトリの場合は、リソースの論理IDまで指定して

 GET /Patient/5/$everything

のように指定して実行できます。 この場合、Patientリソースの論理ID=5に紐づいた関連するリソース(Observation, MedicationRequest, Procedure, Encounter など)を自動的に収集してBundle形式でクライアントに返してくれます。 例えばある患者さんの診療情報を一覧として見せたい場合などには便利な機能です。

$lastn オペレーション

$lastn オペレーションはObservationリソースと組み合わせ使用します。詳細はこちらのページを参照してください。 IRIS for Health のFHIRリポジトリの場合は以下のように指定できます。categoryおよびpatient(またsubject)は必須の指定パラメータとなっています。

 GET /Observation/$lastn?max=10&category=vital-signs&patient=Patient/123

このクエリでは、PatientリソースのID=123の患者に関連した、「vital-signs」カテゴリーに含まれる、最新 10件 のObservationリソースがBundle形式でクライアントに返されます。直近のデータだけを使ってグラフを表示したい、というようなニーズにはぴったりですね。

カスタムオペレーションを作成してみよう

FHIRのオペレーションとはどのようなものか?具体例を2つ挙げてご紹介しました。 カスタムオペレーションはこのようなオペレーションを、プロジェクトのニーズに応じて、自分で定義して実装し利用することができるようになる機能です。定義したカスタムオペレーションはCapability Statementに含めて公開することができます。 以下の手順でカスタムオペレーションを構築することができます。

1. 3つのカスタムクラス(Interactions等)を用意する

まず、最初の手順として、前回のFHIRリポジトリをカスタマイズしよう!パート1で記載した、3つのカスタムクラスを用意します。詳しい内容はパート1の記事をご覧ください。

2. カスタムオペレーション用のクラスを用意し、Interactionsクラスから参照する

Interactionsクラス等と同様に、カスタムオペレーション用のクラスを継承します。 HS.FHIRServer.Storage.BuiltInOperationsを継承し、任意の名称のクラスを作成します。この記事ではCustomFS.MyOperationとします。

(場合によっては、HS.FHIRServer.API.OperationHandlerクラスを継承して作成することもありますが、FHIRリポジトリを利用している場合は、HS.FHIRServer.Storage.BuiltInOperationsを継承し、$everythingなどの実装済みのオペレーションが引き続き使用できるようにする必要があります。詳細はドキュメントをご覧ください。)

これでFHIRリポジトリのカスタマイズに関連して作成したクラスは4つになりましたね。

ベースクラス継承して作成したクラス
HS.FHIRServer.Storage.Json.InteractionsCustomFS.MyInteractions
HS.FHIRServer.Storage.Json.InteractionsStrategyCustomFS.MyInteractionsStrategy
HS.FHIRServer.Storage.Json.RepoManagerCustomFS.MyRepoManager
(new!) HS.FHIRServer.Storage.BuiltInOperationsCustomFS.MyOperation

さらに、このFHIRリポジトリのサーバ処理でこのカスタムオペレーション用のクラスが使用されるように、InteractionsクラスのOperationHandlerClassパラメータでこのクラスを指定します。

Class CustomFS.MyInteractions Extends HS.FHIRServer.Storage.Json.Interactions
{

 Parameter OperationHandlerClass As %String = "CustomFS.MyOperations";

3. カスタムオペレーションの処理を実装する

いよいよ、具体的にカスタムオペレーションの処理内容を実装していきます。2.で作成した、CustomFS.MyOperationsクラス内にメソッドを作成して実装します。

まず作成するオペレーションは影響範囲の応じて3つに分けることができます。 この3つの影響範囲=Scope + オペレーション名により、作成するべきメソッド名が決まってきます。

メソッド名命名ルールについて

メソッド名命名には以下のようなルールがあります。

FHIRScopeOpOperationName

まずメソッド名の先頭は FHIR です。

次に Scope には以下の3つのタイプのいずれかが入ります。

Scope説明
System"ベース" の FHIR エンドポイントに追加する操作を指定します (例えば、http://fhirserver.org/fhir)。これらの操作は、サーバ全体に適用されます。
TypeFHIR エンドポイントにリソース・タイプと共に追加する操作を指定します (例えば、http://fhirserver.org/fhir/Patient)。これらの操作は、指定されたリソース・タイプのすべてのインスタンスで動作します。
Instanceリソースの特定のインスタンスを指す FHIR エンドポイントに追加する操作を指定します (例えば、http://fhirserver.org/fhir/Patient/1)。これらの操作は、リソースの特定のインスタンスでのみ動作します。

以下の図も影響範囲の理解の参考になると思います。

image

そして、"Op" に続き、先頭を大文字にしたオペレーション名が続きます。

例えば、「$deleteall」というカスタムオペレーションをSystemレベルで作成したいなら

FHIRSystemOpDeleteall

というメソッドを作成します。

「$anonymize」というカスタムオペレーションをInstanceレベルで作成したいなら

FHIRInstanceOpAnonymize

というメソッドを作成します。

つまり "FHIR" _ (System or Type or Instance) _ "Op" _ (先頭大文字にしたオペレーション名) という命名ルールですね。

メソッドの実装方法

では、実際に、Instanceレベルの$anonymizeオペレーションを作成していきましょう。このオペレーションの目的はPatientリソースのnameエレメントの中身を匿名化)(*******で埋める)を実装することです。

このメソッドは

 GET /Patient/5/$anonymize

のような形で実装されることを想定しています。

上記の例ででてきたように、FHIRInstanceOpAnonymizeメソッドを以下のように実装します。サンプルなので詳細なエラーハンドリングまでは実装していません。

ClassMethod FHIRInstanceOpAnonymize(pService As HS.FHIRServer.API.Service, pRequest As HS.FHIRServer.API.Data.Request, pResponse As HS.FHIRServer.API.Data.Response)
{
    ///RestClientクラスを利用してFHIRサーバへのアクセスを実行。指定された論理IDのPatientリソースを取得する
	Set clientObj = ##class(HS.FHIRServer.RestClient.FHIRService).CreateInstance(pRequest.SessionApplication)
	Do clientObj.SetResponseFormat("JSON")
	set clientResponseObj=clientObj.Read(pRequest.RequestMethod,pRequest.Type,pRequest.Id)
	
	set pResponse.Json=clientResponseObj.Json
	set pResponse.Status=clientResponseObj.Status
	set pResponse.ETag=clientResponseObj.ETag
	set pResponse.LastModified=clientResponseObj.LastModified
	set pResponse.Location=clientResponseObj.Location
	set pResponse.IsPrettyOut=clientResponseObj.IsPrettyOut

		
	//匿名化処理を実行する
	if pResponse.Status="200" {
        //DynamicObjectからnameエレメントのIteratorを取得し繰り返し処理
		set iter=pResponse.Json.name.%GetIterator()
		while iter.%GetNext(.key,.value) {
			do pResponse.Json.name.%Get(key).%Set("text","***********")
			do pResponse.Json.name.%Get(key).%Set("family","***********")
			do pResponse.Json.name.%Get(key).%Set("given","***********")
		}			
	}
}

パート1のカスタマイズ処理では、実際に検索(GET)されたデータや、送信(POST/PUT)されたデータに対してカスタム処理を実施しましたが、オペレーションの場合はベースとなる検索を実行する必要があります。もちろん、オペレーションの実装目的によっては、検索をする必要がない場合もあるでしょう。

このメソッドでは、得られた検索結果に対して、DynamicObjectのデータ操作を使用して、データを更新し匿名化を行っています。

4. 作成したカスタムオペレーションの定義をCapability Statementに追加する

次に、ロジックを作成したカスタムオペレーションの定義をCapability Statementに追加します。まず、先ほどと同じCustomFS.MyOperationクラスにCapability Statement追加用のAddSupportedOperationsメソッドを記述します。

ClassMethod AddSupportedOperations(pMap As %DynamicObject)
{
	Do ##super(pMap)
    Do pMap.%Set("anonymize","http://myfhirserver/fhir/OperationDefinition/patient-anonymize")
}

メソッド内の、pMap.%Setの最初の引数は"オペレーション名"、2番目の引数はそのオペレーションの定義を表すURIを記載します。 2番目の引数は例えば、先述のPatient/[id]/$everything オペレーションであれば、こちらのページに記載されているように

http://hl7.org/fhir/OperationDefinition/Patient-everything

となります。例えば、あるデータ連携プロジェクトに基づいて実装した場合は、そのプロジェクトの実装ガイド内で規定されたURIになるでしょうし、自プロジェクト内で利便性のために構築したということであれば、任意のURIを指定することもできます。

次にコマンドラインユーティリティを起動して、この変更を反映します。 以下はCUSTOMFSネームスペース内で実行した例になります。

USER>zn "customfs"
 
CUSTOMFS>d ##Class(HS.FHIRServer.ConsoleSetup).Setup()
Query returns no results
HS.FHIRServer.Installer:InstallNamespace Created FHIR web application
HS.FHIRServer.Installer:InstallNamespace Created FHIR API web application
What do you want to do?
  0)  Quit
  1)  Create a FHIRServer Endpoint
  2)  Add a profile package to an endpoint
  3)  Display a FHIRServer Endpoint Configuration
  4)  Configure a FHIRServer Endpoint
  5)  Decommission a FHIRServer Endpoint
  6)  Delete a FHIRServer Endpoint
  7)  Update the CapabilityStatement Resource
  8)  Index new SearchParameters for an Endpoint
  9)  Upload a FHIR metadata package
  10) Delete a FHIR metadata package
Choose your Option[1] (0-10): 7
 
For which Endpoint do you want to update the CapabilityStatement?
  1) /csp/healthshare/customfs/fhir/r4 [enabled] (for Strategy 'CustomFS' and Metadata Set 'hl7.fhir.r4.core@4.0.1')
Choose the Endpoint[1] (1-1): 1

Update the /csp/healthshare/customfs/fhir/r4 service CapabilityStatement to reflect the endpoint strategy. Proceed? (y/n): yes

以上の手順を実行すると、FHIRリポジトリのCapability Statementにカスタムオペレーションのエントリが追加されます! Capability StatementはFHIRリポジトリに以下のリクエストをすると取得できます。anonymizeオペレーションがちゃんと追加されていますね。

 GET http://fhirserver/csp/healthshare/customfs/fhir/r4/metadata
"operation": [
{
    "name": "everything",
    "definition": "http://hl7.org/fhir/OperationDefinition/Patient-everything"
},
(略)
{
    "name": "anonymize",
    "definition": "http://myfhirserver/fhir/OperationDefinition/patient-anonymize"
},

5. 実行して動作を確認してみよう

以上で、カスタムオペレーション $anoymize の実装は完了です。早速動作確認してみましょう!

まずカスタムオペレーションなしで普通にPatientリソースのデータをリクエストしてみます。

 GET http://localhost:52785/csp/healthshare/customfs/fhir/r4/Patient/2

私のデモ環境では、以下のようなnameエレメントを持つデータが格納されていました。

{
    "resourceType": "Patient",
    "id": "2",
    (略)
    "name": [
        {
            "extension": [
                {
                    "url": "http://hl7.org/fhir/StructureDefinition/iso21090-EN-representation",
                    "valueCode": "IDE"
                }
            ],
            "use": "official",
            "text": "山田 太郎",
            "family": "山田",
            "given": [
                "太郎"
            ]
        },
        {
            "extension": [
                {
                    "url": "http://hl7.org/fhir/StructureDefinition/iso21090-EN-representation",
                    "valueCode": "SYL"
                }
            ],
            "use": "official",
            "text": "ヤマダ タロウ",
            "family": "ヤマダ",
            "given": [
                "タロウ"
            ]
        }

次に、実装した $anonymizeを追加して実行してみます!

 GET http://localhost:52785/csp/healthshare/customfs/fhir/r4/Patient/2/$anonymize

以下のように匿名化されたデータが取得できました!

{
    "resourceType": "Patient",
    "id": "2",
    (略)
    "name": [
        {
            "extension": [
                {
                    "url": "http://hl7.org/fhir/StructureDefinition/iso21090-EN-representation",
                    "valueCode": "IDE"
                }
            ],
            "use": "official",
            "text": "***********",
            "family": "***********",
            "given": "***********"
        },
        {
            "extension": [
                {
                    "url": "http://hl7.org/fhir/StructureDefinition/iso21090-EN-representation",
                    "valueCode": "SYL"
                }
            ],
            "use": "official",
            "text": "***********",
            "family": "***********",
            "given": "***********"
        }
    ],

カスタマイズを実装する際のTips

パート1、パート2とIRIS for HealthのFHIRリポジトリをカスタマイズする方法をご紹介してきました。 ここで、カスタマイズの際のTipsを一つご紹介します。

IRISのFHIRリポジトリ処理では、パフォーマンス最適化のために、個別のプロセス内でFHIR処理用のサービスインスタンスが起動され、再利用される仕組みになっています。そのため、条件によっては、サーバ側のカスタム処理を書き換えたのにそれが反映されないことがあります。そのような際にはいくつかの対応方法があります。

  1. FHIR Server Configurationページで「New Service Instance」のチェックを有効にする

開発環境であれば、この方法をお勧めします。FHIRリクエスト毎に新しいService Instanceが立ち上がるので、必ず変更が反映されます。ただし、パフォーマンスへの影響を考えると、本番環境で設定することはお勧めしません。

image

  1. Web Gateway Management 画面からすべてのセッションをクリアする

 System Status画面ですべてのセッションをクリアすることによって、起動しているIRIS側のプロセスもすべて再起動されます。下記画像の黄色の部分をクリックします。

image

  1. 再起動する(常に最強の手段)

まとめ

いかがでしたでしょうか?

IRIS for HealthのFHIRリポジトリカスタマイズ機能を利用することによって、プロジェクト開発で求められる様々な要件に柔軟に対応することができます。もちろん、カスタマイズを多用することが、FHIRの持つ汎用性に影響を及ぼすことを考慮する必要がありますが、パート1/2の記事でご紹介したカスタマイズ機能を効果的に使えば、より高度なFHIRアプリケーションの構築も可能になります。

パート1、パート2の内容を含むIRISのクラスをこちらのGitHubサイトからダウンロードすることができます。

また新たなカスタマイズ手法が実装されたらこちらの開発者コミュニティで紹介したいと思います。

0
0 563
記事 Shintaro Kaminaka · 10月 10, 2021 10m read

開発者の皆さん、こんにちは。 IRIS for Health 2021.1がリリースされてからしばらくたちますが、多くのユーザさんにFHIRリポジトリ機能をお使いいただいています。 今日はFHIRリポジトリのサーバ側の処理をカスタマイズする機能をご紹介したいと思います。

この記事で紹介している内容のFHIRリポジトリカスタマイズに関するドキュメントマニュアルはこちらになります。

この記事はIRIS for Health 2021.1 をベースに記載しています。バージョンによってはカスタマイズに必要なクラスが異なることがあります(例えば2020.1では後述のRepoManagerクラスはまだありません。)

FHIRサーバカスタマイズの2つのアプローチ

FHIRサーバ機能をカスタマイズするには2つのアプローチがあり、1つはこの記事でご紹介する、Interactionクラス群をカスタマイズしてFHIRリポジトリを拡張する方法、もう一つは、相互運用プロダクションでロジックを実装してサーバとしての動作を変更する方法です。

後者は厳密にはFHIRリポジトリとしての実装はそのままで、リポジトリに受け渡される前、あるいはリポジトリから応答を受けた後に、そのデータを参照、変更してカスタマイズを実装する流れになります。 IRISのInteroperability機能(Ensemble)に慣れているかたはこちらのアプローチがわかりやすいかもしれません。しかし、すべてのFHIRリクエストがトレースされメッセージが保存されるため、データ容量やパフォーマンスには気を配る必要があります。 この方法にご興味がある方は、こちらの相互運用プロダクションのドキュメントマニュアルをご参照ください。

FHIRリポジトリを構築する前の作業

FHIRリポジトリカスタマイズで最も注意しなければいけない点、それはFHIRリポジトリを構築する際にカスタマイズができるように、この後記載する手順を実施しておかなければいけないという点です。つまり、デフォルト設定でFHIRリポジトリを構築し、しばらく運用してから、「こういうカスタマイズ処理を組み込みたいな」と思っても、簡単に追加することができないのです。(新しくカスタマイズ可能なリポジトリを構築し、データ移行するなどの方法は取れます)

この制限があるため、FHIRリポジトリ運用当初はカスタマイズ予定がなくても、下記の手順を実行しカスタマイズ機能を組み込んでおくという選択肢もあります。

それでは、そのカスタマイズ方法をご紹介します。 まず、以下の3つのクラスをそれぞれ継承してカスタムクラスを作成します。この記事では共通のパッケージとしてCustomFSとし、クラス名の先頭にはMyを追加しています。

ベースクラス継承して作成したクラス
HS.FHIRServer.Storage.Json.InteractionsCustomFS.MyInteractions
HS.FHIRServer.Storage.Json.InteractionsStrategyCustomFS.MyInteractionsStrategy
HS.FHIRServer.Storage.Json.RepoManagerCustomFS.MyRepoManager

継承したInteractionStrategyクラスとRepoManagerクラスはロジックを実装したりはしませんが、以下の設定を行う必要があります。

  • 両クラスの StrategyKeyパラメータに共通の一意の識別子(文字列ならなんでも)を設定する
  • IntectionStrategyのInteractionClassで自分の作成したInteractionsクラスを指定する
  • RepoManagerのStrategyClassで自分の作成したInteractionStrategyクラスを指定する

図で関連を表現するとこんな感じになります。

image

実際のカスタマイズ内容は継承したInteractionsクラスの各メソッドをオーバーライドして実装していくことになります。

カスタマイズのクイックスタート

以下の表はドキュメントマニュアルに記載されている内容そのままですが、転載します。 これらのメソッドをオーバライドしてロジックを記述し、カスタマイズを行います。

目標HS.FHIRServer.Storage.Json.Interactions のサブクラスのアクション
特定のFHIR相互作用のカスタマイズ相互作用に対応するメソッド(Add,Read,Update)をオーバーライドします。
すべての要求の処理OnBeforeRequest をオーバーライドして、ユーザに透過的なロジックを実装します。FHIR クライアントに要求の処理が異なることを認識させる場合は、カスタムの FHIR 操作を作成します。
すべての要求の後処理OnAfterRequest をオーバーライドして、ユーザに透過的なロジックを実装します。FHIR クライアントに要求の処理が異なることを認識させる場合は、カスタムの FHIR 操作を作成します。
Read 相互作用の結果の後処理PostProcessRead をオーバーライドします ()。
Search 相互作用の結果の後処理PostProcessSearch をオーバーライドします ()。
カスタムの FHIR 操作の追加OperationHandlerClass パラメータをオーバーライドして、HS.FHIRServer.Storage.BuiltInOperations のサブクラスの名前を指定します。"カスタムの FHIR 操作" を参照してください。
バンドルの処理方法のカスタマイズBatchHandlerClass パラメータをオーバーライドして、カスタム・クラスの名前を指定します。既定のハンドラ・クラスは HS.FHIRServer.DefaultBundleProcessor です。
OAuth トークンの処理方法のカスタマイズOAuth2TokenHandlerClass パラメータをオーバーライドして、カスタム・クラスの名前を指定します。既定のハンドラ・クラスは HS.FHIRServer.Util.OAuth2Token* です。

この表のリンク先ドキュメントマニュアルにもいくつか、実装例が記載されていますが、この記事ではもう少しシンプルなパターンのサンプルを記載していきたいと思います。

OnAfterRequestメソッド

まずはOnAfterRequestメソッドを題材にどんな引数を受け取るのか見てみましょう。

Method OnAfterRequest(pFHIRService As HS.FHIRServer.API.Service, pFHIRRequest As HS.FHIRServer.API.Data.Request, pFHIRResponse As HS.FHIRServer.API.Data.Response)
{

このメソッドは、3つの引数を受け取ります。主に使うのはpFHIRRequestとpFHIRResponseの二つです。 このメソッドは OnAfterRequestメソッドなので、FHIRリポジトリ上での処理が完了した後に起動されるメソッドです。そのため、FHIRリポジトリへの要求であるpFHIRRequestと、これから要求元へ返すpFHIRResponseの二つを参照できます。

FHIRリポジトリへのJSONリクエストおよびレスポンスは

pFHIRRequest.Json
pFHIRResponse.Json

に含まれます。これはJsonの構造通りにデータアクセスできる %DynamicObject型の変数なので、このようにアクセスできます。

pFHIRResponse.Json.resourceType //-> Bundleなど
pFHIRResponse.Json.total //->Bundleの場合にそのBundleに含まれる数(10など)
pFHIRResponse.Json.entry.%Get(0).resource.gender //->Bundle内の最初のPatientリソースの性別を取得

ログなどの目的でこの内容を文字列あるいはストリームに保存しておきたい場合は、%ToJSON()メソッドを使って変換します。 他にもこれらのインスタンスは

プロパティ含んでいるデータ
pFHIRResponse.Id追加・更新されたリソースの論理ID
pFHIRResponse.VId追加・更新されたリソースのバージョンID
pFHIRResponse.Status処理のステータス

など様々な情報を含んでいます。詳細はHS.FHIRServer.API.Data.RequestHS.FHIRServer.API.Data.Responseのクラスリファンレスドキュメントをご覧ください。

OnBeforeRequest/PostProcessReadメソッドを実装してみる

それでは具体的にOnBeforeRequestメソッドを使って実装してみましょう。

IRIS for Health のFHIRリポジトリでは、こちらの記事(IRIS for Health上でFHIRリポジトリ+OAuth2認可サーバ/リソースサーバ構成を構築するパート3(OAuth2スコープ編))で紹介したOAuth2アクセストークンのScopeを使う方法で、このScopeのユーザにはこのリソースの参照権限だけ与える、こちらには更新権限を与える、といったアクセスコントロールが可能です。ただ、これはあくまでもリソース単位のコントロールになるので、「このリソースにこういうデータがあるときは登録させたくない」「このリソースにこういうデータが含まれているときは応答として返したくない」などの要望の実装はこのカスタマイズを使って実現することができます。

ではまず、Patientリソースの登録時に、電話番号情報を格納するtelecom要素の最初のデータに 401,403,400 で始まる電話番号が入っていたらエラーを返す、というロジックを実装してみたいと思います。最初の3桁はエラーとして返すHTTPエラーコードと紐づけています。

以下のようなコードになります。本来はチェックロジックなども含めるべきですが、サンプルのためシンプルなコードにしています。

Method OnBeforeRequest(pFHIRService As HS.FHIRServer.API.Service, pFHIRRequest As HS.FHIRServer.API.Data.Request, pTimeout As %Integer)
{
	//POST or PUTでPatientが対象の場合
	if ((pFHIRRequest.RequestMethod="POST")||(pFHIRRequest.RequestMethod="PUT"))&&(pFHIRRequest.RequestPath["Patient") {
	
		if $IsObject(pFHIRRequest.Json) {			
			s tele=pFHIRRequest.Json.telecom.%Get(0).value
			
			if $Extract(tele,1,3)="401" $$$ThrowFHIR($$$HttpOnlyResponse(401))
			if $Extract(tele,1,3)="403" $$$ThrowFHIR($$$HttpOnlyResponse(403))
			if $Extract(tele,1,3)="400" $$$ThrowFHIR($$$GeneralError, "あなたの電話番号は登録する権利がありません。", $$$OutcomeNotSupported(400))

		}

	}

エラーとして応答を返す場合は、このサンプルのように直接Throwすることができます。

$$$ThrowFHIR($$$HttpOnlyResponse(401))

次はPostProcessReadメソッドです。 先ほどは登録時の電話番号をチェックして応答を返しましたが、今回は返すデータをチェックして40で始まっている場合は、マスクして応答するようにしたいと思います。

Method PostProcessRead(pResourceObject As %DynamicObject) As %Boolean
{
	//40で始まる電話番号を持つPatientのtelecom要素を上書きする
	if pResourceObject.resourceType="Patient" {
		if $Extract(pResourceObject.telecom.%Get(0).value,1,2)="40" {
			//return 0
			set pResourceObject.telecom.%Get(0).value="******"
		}
	}
	
	return 1
}

}

この方法ではあくまで、応答を返す際に値を上書きしているだけなので、リポジトリ上の値は変更していないことに注意してください。 また、このPostProcessReadは、リソースのIDまで指定して検索するようなケースにだけ有効です。

http://localhost:52785/csp/healthshare/customfs/fhir/r4/Patient/3

以下のように全件取得や条件取得の際は応答がBundleとして返されるので、この変換は適用されません。

http://localhost:52785/csp/healthshare/customfs/fhir/r4/Patient
http://localhost:52785/csp/healthshare/customfs/fhir/r4/Patient?gender=female

Bundleに含まれる応答をカスタマイズする場合には OnProcessSearch をカスタイズします。

まとめ

いかがでしたでしょうか? 実際のFHIRアプリケーション開発においては、標準のFHIR仕様の範囲だけで実装することが難しい場合は、今回ご紹介したカスタマイズ機能を適切に使用して、開発に役立てることができます。 次回は、カスタマイズ機能パート2として、カスタムオペレーションの実装例をご紹介したいと思います。

0
1 593
お知らせ Shintaro Kaminaka · 7月 8, 2021

開発者の皆さん、こんにちは!

HL7v2メッセージをFHIR(Fast Healthcare Interoperability Resources)に変換するニーズがあり、その変換プロセスが複雑で分かりにくいと感じたことはありませんか?インターシステムズは、HL7v2メッセージをFHIR(Fast Healthcare Interoperability Resources)に変換するプロセスを簡単にする、HealthShareメッセージ変換サービス(HealthShare Message Transformation Services)と呼ばれる新しいクラウドベースのSaaSサービスを展開しています。 この新しいサービスの早期アクセス・プレビュー・プログラムを発表できることを嬉しく思います。 必要なのは、無料のAWSアカウントと、HL7v2メッセージをドロップするためのS3バケット、そしてFHIR出力を得るための別のS3バケットだけです。

この機能の簡単なデモをご覧ください。

<iframe allow="encrypted-media" allowfullscreen="" frameborder="0" gesture="media" height="315" scrolling="no" src="https://www.youtube.com/embed/Js0SDUZbXv8" width="560"></iframe>

インターシステムズ社のラーニングサイトでは、AWS の無料アカウントとインターシステムズ社のクラウド・ポータルの無料アカウントにサインアップして、変換サービスの強力な機能を利用するための簡単なステップ・バイ・ステップ・ガイドを提供しています。 完全なドキュメントは、インターシステムズ社のドキュメントでご覧いただけます。

このサービスは、7月下旬に正式に開始される予定です。プレビューが終了しても、最初の100万件の変換を無料で利用することができます。

インターシステムズ社のこの新しいオファーの詳細は以下の内容です:

HealthShareメッセージ変換サービスの紹介。

医療IT業界では、FHIR® (Fast Healthcare Interoperability Resources)が、ヘルスケアデータを交換するための最新のデータ標準として採用されています。 オンデマンドのHealthShareメッセージ変換サービスを利用することで、医療機関、保険会社、製薬会社は、既存のデータフォーマットをFHIR規格に変換し、データから最大限の価値を引き出すことができます。 インターシステムズは、最新のFHIR規格だけでなく、HL7v2、X12、CDA、C-CDA、DICOMを含むすべての主要なヘルスケア規格を実装しており、ヘルスケアの相互運用性におけるリーダー的存在です。

HealthShareメッセージ変換サービスは、これらの以前の標準からFHIR R4へのメッセージ変換を簡単に行えるように設計されており、初期リリースではHL7 v2メッセージのFHIR R4への変換をサポートしています。 FHIRメッセージは、AWS S3バケットまたはAmazon HealthLake (Preview)に送ることができ、将来的には他のFHIRリポジトリオプションも追加される予定です。

HealthShareメッセージ変換サービスは、HL7v2メッセージをFHIRに変換することを簡単にします。 変換ロジックを気にする必要がないので、メッセージ変換の複雑な作業はインターシステムズに任せて、優れたヘルスケア・アプリケーションの構築に集中することができます。このサービスが提供するのは

  • AWS上での簡単なプロビジョニングと起動
  • インバウンドS3バケットのHL7v2メッセージのチェック
  • HL7コンテンツのバリデーション
  • FHIR R4へのメッセージ変換
  • 変換されたメッセージを、アウトバウンドの S3 バケット、InterSystems FHIR Accelerator (Preview) サービス、または Amazon HealthLake (Preview) リポジトリにルーティングする
  • 変換パイプラインのステータスと統計情報のモニタリング

さらに、このサービスはAWSのインフラ上に構築されているため、ISO 27001:2013およびHIPAAをサポートするHITRUST認証を取得しています。インターシステムズは、このサービスの運用、監視、バックアップを管理しています。

そして何より、このサービスが商業的に開始されると、最初の100万回の変換が無料で提供され、その後は使用した分だけを支払うことになり、変換されたメッセージあたりのコストは非常に低く抑えられます。 このサービスを利用するための長期契約はなく、いつでも解約できます。

皆様からのフィードバックをお待ちしております。 この記事のコメント欄にご意見をお寄せいただくか、 HMTS@InterSystems.com まで直接お問い合わせください。

HL7 ® と FHIR ® は Health Level Seven International の登録商標であり、これらの商標の使用は HL7 による推奨を意味するものではありません。FHIR商標の使用は、HL7によるHealthShare Message Transformation ServicesまたはHealthShare Message Transformation Servicesの推奨を意味するものではありません。

0
0 508
記事 Shintaro Kaminaka · 4月 19, 2021 18m read

開発者の皆さん、こんにちは。

このシリーズでは、IRIS for Healthの使い方ではなく、関連技術として、FHIRプロファイル作成ツールであるSUSHIの握り方使い方を紹介していきたいと思います。

このツールをうまく使うことで、FHIRプロジェクトのプロファイル情報(仕様や制限、拡張などの情報)をうまく整理し、公開することができます。

その前にSUSHIとは何でしょうか?簡単にですが、順番に説明していきたいと思います。

FHIR って?

FHIRとは Fast Healthcare Interoperability Resources の略であり、Web通信の一般的技術であるRESTを使用して、可読性が高く取り扱いがし易いJSON/XML形式のデータの集合(=リソース)をやり取りする短期間で実装可能な医療情報交換標準規格、という定義になっています。

簡単に言えば、医療のデータの表現方法として皆で共通したフォーマットを使うことによって、システム間や施設間などでの情報の伝達や交換をやりやすいようにしよう!ということですね。

FHIRには様々なリソースが定義されています。例えば患者さんの情報にはPatientリソースという定義があり、これを使って表現されます。

FHIR公式サイトには多くのサンプルが掲載されていますので、一部抜粋してみます。 例えばこのようなJSON形式で表現されます。患者番号(Identifier)、氏名(name)、性別(gender)などが表現されています。

{
  "resourceType": "Patient",
  "id": "pat1",
  "text": {
    "status": "generated",
    "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n      \n      <p>Patient Donald DUCK @ Acme Healthcare, Inc. MR = 654321</p>\n    \n    </div>"
  },
  "identifier": [
    {
      "use": "usual",
      "type": {
        "coding": [
          {
            "system": "http://terminology.hl7.org/CodeSystem/v2-0203",
            "code": "MR"
          }
        ]
      },
      "system": "urn:oid:0.1.2.3.4.5.6.7",
      "value": "654321"
    }
  ],
  "active": true,
  "name": [
    {
      "use": "official",
      "family": "Donald",
      "given": [
        "Duck"
      ]
    }
  ],
  "gender": "male",
  "以下略"
}


FHIRプロファイルって?

FHIRではJSONやXMLという表現形式だけではなく、何の情報をどのようなJSONキー名称で記載するか、どのようなコードを使用するか、どのような構造で表現するかいった決まりが存在します。それをFHIRプロファイルと呼んでいます。

プロファイルは用語として色々な意味で使われています。

広義では・・・
FHIRリソースおよびFHIRサーバに関する制約の定義の集合。それを表すアーティファクト(成果物)。
狭義では・・・
あるリソースに対して、特定の制約を適用したコンフォーマンス・リソース(適合性リソース) 。 この場合、プロファイルはリソース単位に存在する(例)Pateintプロファイル、Observationプロファイル…

                    詳細については、こちらのFHIRプロファイルに関するJapan Virtual Summit 2021動画をご覧ください。(約20分)

FHIRの公式Webサイトでは各リソースについて既定の仕様が開示されています。ですが、各リソースの使い方の自由度がとても高く、そのままでは実際に相互運用性のあるデータ交換をすることはこ困難です。ですので、事前の「申し合わせ」にもとづいて、リソースの記述に「規則」を改めて設ける必要があります。この「申し合わせ」「規則」に相当するのは、実装ガイドライン(Implementation Guide)とプロファイル(Profile)に相当します。

実装ガイドラインは主に文章で記述されたもので、WordやExcel、HTML等でも記述されています。一方、ProfileはFHIRのStructureDefinitionリソースを使って計算可能なJSON形式で記述をしています。このFHIRのプロファイル自体もFHIRのリソースで表現できる、というのがFHIRの特徴の一つでもあります。例えばIRIS for Healthのような製品でその定義を取り込んで、機能拡張ができるように、JSON形式で仕様まで表現できるようになっているのです。

実装ガイドラインは様々なツールで作れますが、米国HL7協会はProfileから実装ガイドラインを自動的に生成するIG Publisherを公開しています。このツールを使えば、米国HL7協会が出しているフォーマットで実装ガイドラインのHTMLファイル等を生成することができます。この記事後半ではその方法についても紹介しています。

例えばこれは、US Coreと呼ばれる米国で標準的に使用されることが推奨されたPatientリソースの記法上の規約を表現した、「StructureDefinition」というリソースです。 (引用元)

{
  "resourceType" : "StructureDefinition",
  "id" : "us-core-patient",
  "text" : {
    "status" : "extensions",
    "div" : "省略"
  },
  "url" : "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient",
  "version" : "3.1.1",
  "name" : "USCorePatientProfile",
  "title" : "US Core Patient Profile",
  "status" : "active",
  "experimental" : false,
  "date" : "2020-06-27",
  "publisher" : "HL7 US Realm Steering Committee",
  "contact" : [
    {
      "telecom" : [
        {
          "system" : "url",
          "value" : "http://www.healthit.gov"
        }
      ]
    }
  ],
  "以下略"

FHIRプロファイルを表現されるリソースとしては他にもImplementationGuideやFHIRサーバの一連の機能をまとめたCapabilityStatementなどがあります。

FHIR Shorthand とは?

というわけで、ではFHIRプロファイルを作成するには↑のJSON構造を作っていけばいいんだな!?ということになる訳ですが、これを手作業でやるのはどう考えて難しいし煩雑ですよね。間違えそうです。

これを補助するためのアプリケーションやツールが公開されており、商用製品やオープンソースなどいくつかの選択肢があります。 こちらのページをご覧ください。

例えばオランダのFirely社のForgeなどは有名ですが、最近ではFHIR Shorthand (リンク)というFHIRアーティファクトを定義するためのドメイン固有の言語も広く使われるようになってきています。

FHIR Shorthandは言語の一種であり、例えば以下のような定義ファイル=FSH(フィッシュ)ファイルを作成しながら、FHIRプロファイルを作成することができます。 以下にサンプルのFSHファイルを例示します。引用元 例えば、このプロファイルの名前(Profile: CancerDiseaseStatus)、ベースとなる元のFHIRリソース(Parent: Observation)、カーディナリティを変更するエレメントの指定(bodySite 0..0)などの内容を含んでいます。

Alias: LNC = http://loinc.org
Alias: SCT = http://snomed.info/sct

Profile:  CancerDiseaseStatus
Parent:   Observation
Id:       mcode-cancer-disease-status
Title:    "Cancer Disease Status"
Description: "A clinician's qualitative judgment on the current trend of the cancer, e.g., whether it is stable, worsening (progressing), or improving (responding)."
* ^status = #draft
* extension contains EvidenceType named evidenceType 0..*
* extension[evidenceType].valueCodeableConcept from CancerDiseaseStatusEvidenceTypeVS (required)
* status and code and subject and effective[x] and valueCodeableConcept MS
* bodySite 0..0
* specimen 0..0
* device 0..0
* referenceRange 0..0
* hasMember 0..0
* component 0..0
* interpretation 0..1
* subject 1..1
* basedOn only Reference(ServiceRequest or MedicationRequest)
 (省略)

SUSHIって?

FHIR/FHIRプロファイル/FHIR Shorthandと順に説明してきましたが、ついにSUSHIの説明です。

SUSHI (an acronym for “SUSHI Unshortens SHorthand Inputs”) (4) is a reference implementation of a FSH compiler that translates FSH into FHIR artifacts such as profiles, extensions, and value sets. SUSHI is installed on your own computer and runs locally from the command line. (引用元)

(訳)SUSHI("SUSHI Unshortens SHorthand Inputs "の略)は、FSHファイルをプロファイル、エクステンション、バリューセットなどのFHIRアーティファクトに変換するFSHコンパイラのリファレンス実装である。SUSHIは自分のコンピュータにインストールされ、コマンドラインからローカルに実行される。

つまり、先ほどのFHIR Shorthandを記述したFSH(フィッシュ)ファイルを、SUSHIで処理すると、StructureDefinitionなどのファイルが生成される、ということです。

この仕組みを説明したわかりやいようで、ちょっとわかりにくい一枚の絵があります!引用元

image

(この絵では、魚を処理(=コンパイル)して、寿司ができるようなイメージで書かれてるんですが、実際はコンパイルをしているのが「SUSHIコンパイラ」で、できあがるのは「プロファイルなどのFHIRアーティファクト」なのでちょっと違うと思うんですよね・・・。お寿司の説明にProfilesやExtensions等の記載はありますけども。)

駆け足でSUSHIとは何か、までご紹介してきましたがご理解いただけたでしょうか?

FSH Schoolに行こう

肝心なSUSHIのインストール方法や基本的な使い方ですが、それらについてはここで説明するよりも、非常に丁寧に紹介されたオフィシャルサイトがありますので、そちらをご紹介したいと思います。 その名もFSH Schoolです。

SUSHIを使って生成されるのは、StrudctureDefinitionなどのリソース(JSONファイル)ですが、同じくこのサイトで紹介されている、"IG Publisherツール"を使うことによって、それらを取りまとめたHTMLのソースまで生成することができます。

image

まずはこのSUSHI Tutorialに内容に沿って、基本的な機能の確認をされることをお勧めします。

うまくいかないときは、ダウンロードできるフォルダに含まれる完成版FSH Tank(!)であるFishExampleCompleteディレクトリを参照されると良いと思います。

私はWindows環境で試しました。Node.jsもインストールされていなかったので、こちらのサイトの情報を参考にさせていただきました。 また、チュートリアルに以下の記載がある通り、IG Publisherを使ったHTMLファイルの出力には Jekyll というツールが必要になります。

Warning
Before proceeding to the next command: If you have never run the IG Publisher, you may need to install Jekyll first. See Installing the IG Publisher for details.

こちらのサイトからJekyllのキット等は入手できます。

SUSHI実行例

私の環境で、チュートリアルの完成版を使ったsushiコマンド実行結果を掲載します。 コマンド等の詳細はこちらのサイト(Running SUSHI)をご覧ください。

>sushi .
info  Running SUSHI v1.2.0 (implements FHIR Shorthand specification v1.1.0)
info  Arguments:
info    C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\fsh-tutorial-master\FishExampleComplete
info  No output path specified. Output to .
info  Using configuration file: C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\fsh-tutorial-master\FishExampleComplete\sushi-config.yaml
info  Importing FSH text...
info  Preprocessed 2 documents with 3 aliases.
info  Imported 4 definitions and 1 instances.
info  Checking local cache for hl7.fhir.r4.core#4.0.1...
info  Found hl7.fhir.r4.core#4.0.1 in local cache.
info  Loaded package hl7.fhir.r4.core#4.0.1
(node:26584) Warning: Accessing non-existent property 'INVALID_ALT_NUMBER' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)
(node:26584) Warning: Accessing non-existent property 'INVALID_ALT_NUMBER' of module exports inside circular dependency
info  Converting FSH to FHIR resources...
info  Converted 3 FHIR StructureDefinitions.
info  Converted 1 FHIR ValueSets.
info  Converted 1 FHIR instances.
info  Exporting FHIR resources as JSON...
info  Exported 5 FHIR resources as JSON.
info  Assembling Implementation Guide sources...
info  Generated ImplementationGuide-fish.json
info  Assembled Implementation Guide sources; ready for IG Publisher.

╔════════════════════════ SUSHI RESULTS ══════════════════════════╗
║ ╭──────────┬────────────┬───────────┬─────────────┬───────────╮ ║
║ │ Profiles │ Extensions │ ValueSets │ CodeSystems │ Instances │ ║
║ ├──────────┼────────────┼───────────┼─────────────┼───────────┤ ║
║ │    2     │     1      │     1     │      0      │     1     │ ║
║ ╰──────────┴────────────┴───────────┴─────────────┴───────────╯ ║
║                                                                 ║
╠═════════════════════════════════════════════════════════════════╣
║ It doesn't get any betta than this!    0 Errors      0 Warnings ║
╚═════════════════════════════════════════════════════════════════╝

実行するとFSHファイルのコンパイルが実行され、最後にいくつのProfilesやExtensionが生成されたか、表示されます。問題なければ、"info"だけが表示されますが、FSHファイルの定義に誤りがあると、WarningやErrorも表示されます。エラーメッセージは比較的親切で何が問題が把握しやすいと思います。(個人的には最後の表に掲載される、なぞの「魚一言?」みたいな一文が楽しみです。)

実行後には、プロジェクトフォルダ内のfsh-generatedフォルダにStructureDefinitionのJSONファイルが生成されているのが確認できます。

続いて、「_updatePublisher」コマンドで、IG Publisherツールを入手し、「_genonce」コマンドでIG Publisherを起動し、HTMLファイル群も生成してみます。この実行ログは長いので割愛します。

実行後、同じプロジェクトフォルダ内の output フォルダを確認すると多くのファイルが生成されているのがわかります。index.htmlファイルを開くと以下のようなページが生成されていることが確認できます。

image

このようなFHIR公式サイトでも見慣れた、リソースの説明ページなども自動生成されます。

image


## Implementation Guide(実装ガイド)を書いてみよう

私もこのツール群を触り始めて日が浅いですが、簡単なスタートアップとして、実装ガイドを記述していく方法をご紹介したいと思います。

詳細な使い方についてはFSH Schoolサイト内の情報をご覧ください。こちらで紹介されているのFHIR DevDaysのスライド等も大変参考になると思います。

まず sushi --init コマンドでプロジェクト構造のひな型を作りましょう。

C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject>sushi --init

╭───────────────────────────────────────────────────────────╮
│ This interactive tool will use your answers to create a   │
│ working SUSHI project configured with your project's      │
│ basic information.                                        │
╰───────────────────────────────────────────────────────────╯

Name (Default: ExampleIG): MyFirstSUSHIProject
Id (Default: fhir.example): myfirstsushi
Canonical (Default: http://example.org): http://example.org/myfirstsushi
Status (Default: draft):
Version (Default: 0.1.0):
Initialize SUSHI project in C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject\MyFirstSUSHIProject? [y/n]: y
Downloading publisher scripts from https://github.com/HL7/ig-publisher-scripts
(node:13972) Warning: Accessing non-existent property 'INVALID_ALT_NUMBER' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)
(node:13972) Warning: Accessing non-existent property 'INVALID_ALT_NUMBER' of module exports inside circular dependency

╭───────────────────────────────────────────────────────────╮
│ Project initialized at: ./MyFirstSUSHIProject             │
├───────────────────────────────────────────────────────────┤
│ Now try this:                                             │
│                                                           │
│ > cd MyFirstSUSHIProject                                  │
│ > sushi .                                                 │
│                                                           │
│ For guidance on project structure and configuration see   │
│ the SUSHI documentation: https://fshschool.org/docs/sushi │
╰───────────────────────────────────────────────────────────╯


実行すると必要最低限の設定ファイルやFSHファイルが作成されます。 次に少し修正をしてみましょう。

その前にエディタの紹介です。fshファイルを修正するためのExtensionが公開されているので、Visual Studio Codeの使用がおすすめです。

せっかくなので、日本語の情報を入力してどのように反映されるか見ていきたいと思います。

まず、sushi-config.yaml を修正します。実装ガイドのタイトルを追加し、メニュー画面も日本語表記に変更した上で、コンテンツ一覧ページ(tuc.html)とカスタムページ(mycustompage.html)を追加しています。

sushi-config.yaml

# ╭──────────────────────────────────────ImplementationGuide───────────────────────────────────────╮
# │  The properties below are used to create the ImplementationGuide resource. For a list of       │
# │  supported properties, see: https://fshschool.org/sushi/configuration/                         │
# ╰────────────────────────────────────────────────────────────────────────────────────────────────╯
id: myfirstsushi
canonical: http://example.org/myfirstsushi
name: MyFirstSUSHIProject
# titleを追加して、ページ上部に表示されるようにします。
title: ○○FHIRプロジェクト 実装ガイド
status: draft
publisher: InterSystems Japan/S.Kaminaka
description: SUSHIを使ったFHIRプロジェクト実装ガイドのサンプルです。
version: 0.1.0
fhirVersion: 4.0.1
copyrightYear: 2021+
releaseLabel: ci-build

# ╭────────────────────────────────────────────menu.xml────────────────────────────────────────────╮
# │  To use a provided input/includes/menu.xml file, delete the "menu" property below.             │
# ╰────────────────────────────────────────────────────────────────────────────────────────────────╯
# メニューを日本語表示されるように変更します。
menu:
  実装ガイドホーム: index.html
  コンテンツ一覧: toc.html
  FHIRアーティファクトサマリ: artifacts.html
  カスタムページ: mycustompage.html

インデックスページや、カスタムページはマークダウンで記述できます。

index.md

# MyFirstSUSHIProject

Feel free to modify this index page with your own awesome content!

### プロジェクトの背景

pagecontent/index.md ファイルを変更して、htmlファイル内の記載内容を変更できます。

ページを記述にはマークダウン記法を使用することができます。

### 参考情報へのリンク
 (略)

mycustompage.md

## これはカスタムページです。

マークダウンファイルを用意しておくとhtmlファイルが生成されます。

プロジェクトに応じたページを生成し、実装ガイドに含めることができます。

最後に最も重要なFSHファイルを修正します。このひな形には、Patientプロファイル用のFSHファイルが含まれているので、それを少しだけ修正しました。

patient.fsh

// This is a simple example of a FSH file.
// This file can be renamed, and additional FSH files can be added.
// SUSHI will look for definitions in any file using the .fsh ending.
Profile: MyPatient
Parent: Patient
Title: "○○プロジェクトのPatientプロファイル"
* name 1..* MS
// ^shortを変更して、一覧表示画面の説明部分を変更できます。
* name ^short = "患者さんの氏名を格納します。"
* name ^definition = "患者さんの氏名を格納するエレメント。NeXEHRS JP COREに準拠した漢字・カナ記法を使用します。"

それでは、以下のコマンドを実行して、生成された実装ガイドページを確認してみましょう。

sushi.

_updatePublisher

_genonce

以下のような情報を含むページが簡単に生成できます。

あわせてStructureDefinitionやImplementationGuideなどのJSONファイルももちろん生成されています。

image

image

まとめ

いかがでしたでしょうか? 単にFHIRプロファイルのJSONファイルを生成するだけでなく、HTMLファイルを生成する機能も付随しているので、このツールをうまく使えばFHIRの仕様を分かりやすく伝えることが可能なコンテンツが簡単に作成できるのではないかと考えています。

このツール自体はInterSystemsとは直接関係ない製品ではありますが、FHIRプロジェクトの情報交換に役立つツールということで、この開発者コミュニティでご紹介させていただきました。

この記事を見て試していただいた方、あるいはすでに使いこなしている方、使い方のコツや便利な機能やシンタックスなどをご紹介いただけると嬉しく思います。

次回のこのシリーズでは、SUSHIで生成したSearch Parameterの定義ファイルをIRIS for Healthで読み込んで、検索パラメータを拡張する内容に取り組んでみたいと考えています。

(2021/4/20 特にプロファイルの説明について、説明が曖昧な箇所をご指摘いただきましたので修正しました。ご指摘ありがとうございました。)

0
2 2623
記事 Shintaro Kaminaka · 4月 15, 2021 9m read

開発者の皆さん、こんにちは。 以前の記事でIRIS for Health上でFHIRリポジトリを構築し、OAuth2認証を構成する方法をご紹介しました。

この代行認証編では、IRIS for HealthのFHIRリポジトリに組み込まれた認証機能ではなく、IRISの代行認証機能+ZAUTHENTICATEルーチンを使用して認証を行う方法をご紹介します。

前回記事でご紹介したように、標準のFHIRリポジトリの認証機構では、アクセストークンの発行先を追加するためのAudienceの指定(aud=https://~) や、アクセストークンだけではなくベーシック認証の情報を送付するなどの対応が必要でした。

スクラッチでFHIRクライアントを開発するのではなく、既成の製品やアプリケーションからアクセスする場合、上記のような処理の実装ができないことがあるかもしれません。 そのような場合には、この代行認証+ZAUTHENTICATEルーチンを使用して、カスタマイズした認証の仕組みを構築することができます。

この記事に含まれる情報のドキュメントについて

この記事で記載されている情報はIRIS for Healthのドキュメントにも含まれている内容をわかりやすく再構成したものです。

RESTサービスの保護:RESTアプリケーションおよびOAuth2.0

OAuth 2.0 クライアントとしての InterSystems IRIS Web アプリケーションの使用法


代行認証を有効にする

まず使用しているIRIS環境で「代行認証」機能を有効にし、アクセスするFHIRリポジトリの「Webアプリケーション設定」で「代行認証」機能を使える用に構成します。


認証/ウェブセッションオプション画面

まずシステムとして「代行認証」が使用できるように構成します。

管理ポータルの システム管理→セキュリティ→システム・セキュリティ→認証/ウェブセッションオプション と進み、「代行認証を許可」をチェックします。 「代行認証によるOS認証を許可」ではありませんのでご注意ください。

image


%Service_WebGatewayサービス 画面

次に、CSPゲートウェイを経由したWebのアクセスに対して、「代行認証」が有効になるよう構成します。

管理ポータルの システム管理→セキュリティ→サービス と進み、「%Service_WebGateway」をクリックして、許可された認証方法の「代行」にチェックがついていることを確認します。もしチェックされていなければ、チェックして保存を実行してください。

image


FHIRリポジトリの ウェブアプリケーションの編集 画面

最後に、アクセスするFHIRリポジトリの ウェブ・アプリケーションの編集画面で「代行認証」を有効にします。

管理ポータルの システム管理→セキュリティ→アプリケーション→ウェブ・アプリケーション と進み、該当のFHIRリポジトリアプリケーションを選択します。 特に変更をしていなければ、/csp/healthshare/<namespace>/fhir/r4 となっています。

この画面で、セキュリティの設定:許可された認証方法の「代行」をチェックして保存します。

image

これで、「代行認証」を利用する準備はOKです。次は、実際に代行認証のためのロジックが記載されたZAUTHENTICATEルーチンを用意します。


ZAUTHETICATEルーチンの入手とインポート

ZAUTHENTICATEルーチンのサンプルはInterSystemsのGitHubで公開されています。

GitHub:Samples-Security

この記事ではここで紹介されているREST.ZAUTHENTICATE.macルーチンを利用します。 GitHubのREADMEに記載されているこのルーチンの説明をここにも転載します。

  • REST.ZAUTHENTICATE.mac is another sample routine that demonstrates how to authenticate a REST application using OAuth 2.0. To use this sample:
    1. Configure the resource server containing the REST application as an OAuth 2.0 resource server.
    2. Copy this routine to the %SYS namespace as ZAUTHENTICATE.mac.
    3. Modify value of applicationName in ZAUTHENTICATE.mac.
    4. Allow delegated authentication for %Service.CSP.
    5. Make sure that the web application for the REST application is using delegated authentication.

この記事では、先に手順の4.,5.を済ませているので、ルーチンのインポートを実施しましょう。 (上記READMEでは、%Service.CSPと記載されていますが、現在は%Service_WebGatewayになっています。)

GitHubからルーチンをダウンロードしてインポートするか、あるいは、このリンクから直接ルーチンを表示し、中身をStudioやVS Codeのエディタを使ってコピーしてZAUTHENTICATEルーチンをつくることもできます。%SYSネームスペースに作成します。

(注意:2021/4/16時点ではこのルーチンをスタジオからインポートするとエラーが発生してしまいます。お手数ですが、ファイルの中身をコピーしてZAUTHENTICATEルーチンを作成する方法で回避してください。)

ZAUTHENTICATEルーチンを作成したら、applicationNameを変更します。これは前回の記事で記載したOAuth2クライアントアプリケーションの クライアント構成 画面で作成した「アプリケーション名」を指定します。

image

ここでは前回の記事にならい「FHIRResource」としています。コードの一部を紹介します。

// Usually you will need to modify at least the roles to be assigned.
set roles="%DB_DEFAULT,%Operator"

$$$SysLog(3,"OAuth2","[ZAUTHENTICATE]","ServiceName="_ServiceName_", Username="_Username_", roles="_roles)

// MUST BE MODIFIED FOR EACH INSTANCE WHICH USES OAuth 2.0 for REST.
// The application name used to define the authorization server to this resource server.
set applicationName="FHIRResource"

set loginSuccessful=0
set errorText=""

コードを変更したらコンパイルを実行します。

このZAUTHENTICATEルーチンで重要なのは以下のコード部分です。 GetAccessTokenFromRequestメソッドを使用してHTTPリクエストからアクセストークンを取り出し、ValidateJWTメソッドを使用してValidationを実施し正しいアクセストークンであることを確認しています。

// This ZAUTHENTICATE routine will support OAuth 2.0 based
// delegated authentication for subclasses of %CSP.REST.
set accessToken=##class(%SYS.OAuth2.AccessToken).GetAccessTokenFromRequest(.sc)

// Check if authorized.
// if the access token is not a JWT, we would need to validate the access token
// using another means such as the introspection or userinfo endpoint.
if $$$ISOK(sc) {
	set valid=##class(%SYS.OAuth2.Validation).ValidateJWT(applicationName,accessToken,,,.jsonObject,,.sc)
}

POSTMANからのテスト

それでは前回同様、RESTクライアントツールのPOSTMANからテストしてみましょう。 前回同様、まずはアクセストークンを取得します。

前回とは異なり、Auth URLにaudパラメータを追加する必要はありません。トークンを取得できたら、「Use Token」ボタンをクリックし、そのトークンを使用できるようにします。

次は、FHIRリポジトリへのアクセスです。今回は前回と異なり、ベーシック認証と組み合わせる必要はありませんので、そのままFHIRリポジトリにアクセスするRESTのURLのみを入力し、実行します。

FHIRリソースが取得できたら成功です。

2020.4以降の対応

IRIS for Health 2020.4ではこちらの記事に掲載したように、FHIRリポジトリ上でアクセストークンのスコープ情報がチェックされるようになりました。 このため、セキュリティ用件にも依存しますが、ZAUTHENTICATEルーチンで必ずしもアクセストークンのValidationチェックを行う必要はありません。 これまでこのシリーズで紹介してきましたように、IRIS for HealthがOAuth2認可サーバの役割も兼ねている場合、2020.4上で動かす最も単純な方法は、ZAUTHENTICATEルーチンのGetCredentialsラベルで、アクセストークンを取得する際にも指定したIRISパスワードユーザを返すようにすることです。

例:アクセストークンを取得した際のユーザと image

同じユーザを返すようにする。(このdaikoユーザには%All権限を与えています)

GetCredentials(ServiceName,Namespace,Username,Password,Credentials) Public {
	if ServiceName="%Service_WebGateway" {
		// Supply user name and password for authentication via a subclass of %CSP.REST
		set Username="daiko"
		set Password="xxxxx"
	}
	quit $$$OK
}

こちらの代行認証に関するドキュメントに記載があるように、GetCredentialsラベルで実在するIRISパスワードユーザが返された場合はそのユーザに認証が行われるため、ZAUTHENTICATEルーチンで実行されていたアクセストークンのValidationチェックのロジックは実施されなくなります。 ただし、アクセストークンの検証はその後FHIRリポジトリ上で実施されるため不正なアクセストークンでアクセスしたりすることはできません。 なお、2020.4では、スコープのチェックやAudience情報のチェックも行われるため、このバージョンの代行認証では aud=https://~ の情報の追加や適切なスコープ指定も必要になります。

テストとしてはこの方法で動作を確認することができると思いますが、もちろん実際のアプリケーションで実装する場合は、より複雑な状況を考慮に入れる必要があるでしょう。例えば、アクセスするユーザごとに異なるIRISパスワードユーザをもつケースもあれば、そもそもアクセストークンの発行元であOAuth2認可サーバが、IRISではなく他のサービスである可能性もあります。後者のようなケースでは、このZAUTHENTICATEルーチン上で代行ユーザを作成する必要があり、さらにそのユーザ名はアクセストークン内のユーザ情報(sub)と一致する必要があります。

残念ながらこの記事でそれらの状況をすべてカバーすることはできませんが、この記事に記載されている情報がIRIS for HealthのFHIRリポジトリを活用したセキュアなアプリケーション構築の一助となれば幸いです。

0
0 474
記事 Shintaro Kaminaka · 4月 15, 2021 7m read

開発者の皆さん、こんにちは。

以前の記事でIRIS for Health上でFHIRリポジトリを構築し、OAuth2認証を構成する方法をご紹介しました。 IRIS for Health 2020.4がリリースされ、FHIRリポジトリのアクセストークンをチェックする機能が追加になりました。

ドキュメントはAccess Token Scopesです。

この記事ではドキュメントの記載も抜粋しながらこの機能を紹介していきます。

Basic Processing

The access token that accompanies a request must include at least one patient clinical scope or user clinical scope, or else the request is rejected with an HTTP 403 error. If an access token contains both a patient clinical scope and a user clinical scope, the FHIR server enforces the patient clinical scope while ignoring the user clinical scope.

(訳)リクエストに添付されるアクセストークンには、患者のクリニカル・スコープまたはユーザーのクリニカル・スコープが少なくとも1つ含まれていなければならず、そうでなければリクエストはHTTP 403エラーで拒否されます。アクセストークンに患者のクリニカル・スコープとユーザーのクリニカル・スコープの両方が含まれている場合、FHIRサーバーは患者のクリニカル・スコープを強制し、ユーザーのクリニカル・スコープを無視します。

この機能が追加になったことで、以前の記事でご紹介していたような、"scope1"のようなスコープ指定では403エラーとなり、FHIRリポジトリから応答を受け取ることはできなくなりました。(前回の構成が残っている方はぜひ2020.4にアップグレードして403エラーになることを試してみてください。)

この記事では、正しいスコープを指定して、FHIRリポジトリにアクセスする方法をご紹介します。 ベースとなるOAuth2サーバ構成やクライアント構成に関しては以前と変わりませんので、以前の記事を参考にしてください。


SMART on FHIR : Scopes and Launch Context

今回のIRISの実装は、SMART on FHIRプロジェクトのスコープ定義をベースにしています。

SMART App Launch: Scopes and Launch Context

このサイトのQuick Startに記載されている以下のスコープをベースにこの記事の検証を進めてみたいと思います。 詳細についてはSMART on FHIRの上記解説サイトをご覧ください。

ScopeGrant
patient/*.read現在の患者のあらゆるリソースを読むことができる許可
user/*.*現在のユーザーがアクセスできるすべてのリソースの読み取りと書き込みの許可


OAuth 2.0 認可サーバ構成の変更

まず、OAuth2認可サーバ構成で、サポートしていないスコープが許可されるように構成します。 OAuth2認可サーバの「スコープ」タブで、「サポートしていないスコープを許可」にチェックを入れます。

image


スコープを指定してPatientリソースを登録する

それでまず、Patientリソースを登録してみましょう。 Create Interactionを行う場合には、user clinical scope が必要になります。以下はIRIS for Healthのドキュメントからの抜粋です。

Create Interaction

Requests to create a new Patient resource must include a user clinical scope that gives write permissions (user/Patient.write or user/Patient.*). You cannot perform a create interaction for a Patient resource with a patient clinical scope; patient clinical scopes must include a patient context value, and the create interaction cannot include a resource id.

(訳)新規にPatientリソースを作成する要求には、書き込み権限(user/Patient.writeまたはuser/Patient.*)を与えるユーザークリニカルスコープを含める必要があります。患者臨床スコープには患者コンテキスト値を含める必要があり、作成インタラクションにリソースIDを含めることはできません

先ほど、でてきたスコープ「user/*.*」を追加して、アクセストークンを取得し、Patientリソースを登録してみましょう。

以下の図はRESTツールPOSTMAN上で、スコープを指定している画面です。

image

任意のスコープを許可する設定が正しく動作していれば以下のような確認画面が表示されます。

image

「許可」をクリックして、アクセストークンを取得します。このアクセストークンを使ってPatientリソースを登録してみましょう。 まず登録するPatientリソースを用意してください。FHIR公式ページから取得しても良いですし、過去のコミュニティ記事から取得しても良いです。

POSTMANを使う場合、Bodyタブに登録したいリソースを貼り付けます。

image

前回記事同様、アクセストークン+ベーシック認証を行う必要があるので、取得したアクセストークンを access_toke=eyXXX としてパラメータに追加し、Authorizationタブではベーシック認証を選択してください。

image

メソッドがPOSTであること、リクエストURLが正しくPatientを指定していることなども確認出来たら、登録を実行してみてください。Status 201 Created が返ってきたら登録は成功です。

登録が成功したら、続いてGETも試してみてください。 このスコープは読み取りの権限もありますから取得できるはずです。

リソースが取得できたら、今登録したPatientリソースのリソースIDも確認してください。以下の例では 1 になります。

image


スコープを指定してPatientリソースを取得する

では、Patientリソースが登録できたので、今度は異なるスコープを指定して先ほどのPatientリソースを取得してみましょう。

患者に紐づく情報を取得する場合、patient clinical scope が必要になります。

Patient Clinical Scope / Patient Context Value

If an access token includes a patient clinical scope, it must also include a patient context value (also known as “launch context”) that is the id of a Patient resource. This patient context value provides access to the specified Patient and its related resources.

(訳)アクセストークンに患者の臨床範囲が含まれる場合、Patient リソースの ID である患者コンテキスト値(「起動コンテキスト」とも呼ばれる)も含まれなければならない。この患者コンテキスト値は、指定された患者及びその関連リソースへのアクセスを提供する。

今回はスコープに「patient/*.read」を追加します。もう一点どの患者情報にアクセスできるのか判断するために、PatientリソースのリソースID情報を含むPatient Context Valueをスコープとして渡します。具体的には、「launch/patient/1」(リソースID=1の場合)もスコープとして指定します。

POSTMANでは以下のように指定します。スコープの間は半角スペースで区切ります。

image

アクセストークンを取得できたら、先ほどと同じ手順でリクエストを投げてみましょう。 ただし、今回取得したアクセストークンで取得できるFHIRリソースはリソースID=1のPatientリソースに紐づく情報だけであることにご注意ください。ということは、取得するためにRESTのパスは Patient/1 となります。

例:https://<server>/csp/healthshare/fhirserver/fhir/r4/Patient/1?access_token=eyXXX

無事に先ほどのPatientリソースが取得できたらアクセス成功です! 次は、このトークンが本当にリソースID=1だけに制限されているかも確認してみましょう。他のリソースIDを指定するか、Patientリソース全体を取得するようなリクエストを投げてみてください(ただし存在しないPatientのリソースIDを指定するとそのリソースは存在しませんという別のエラーになります。)

例:https://<server>/csp/healthshare/fhirserver/fhir/r4/Patient/2?access_token=eyXXX 例:https://<server>/csp/healthshare/fhirserver/fhir/r4/Patient?access_token=eyXXX

Status 403エラーになりましたか?エラーが返れば確認は成功です。


まとめ

どのようなFHIRリソースをFHIRリポジトリに格納しているのか?また、どのようなアプリケーションを構築して、どのようにユーザにアクセス制限を設定したいのか?等により、利用すべきスコープの使い方は異なってきます。

安全なFHIRアプリケーションを構築するために、この新しいアクセストークンスコープ機能(とあとこの記事が)活用されると幸いです。

0
0 510
記事 Shintaro Kaminaka · 12月 10, 2020 20m read

以前紹介した記事 (お読みいただいたでしょうか) では、GitHub とパーフェクトに統合する CircleCI のデプロイシステムについてカバーしました。 なのにどうしてさらに掘り下げる必要があるのか? それは、GitHub には GitHub Actions という独自の CI/CD プラットフォームがあり、詳しく見ておく価値があるからです。 GitHub Actions を使えば、外部のサービスを使用する必要はありません。

この記事では、GitHub Actions を使って InterSystems Package Manager のサーバー部分である ZPM-registry を Google Kubernetes Engine (GKE) にデプロイする、ということを試したいと思います。

どのシステムもそうですが、ビルド / デプロイのシステムも結局は「これを実行して、そこに移動したら、あれを実行する」といった感じの流れになります。 GitHub Actions では、こうようなアクションはそれぞれが 1 つ以上のステップで構成されるジョブとして扱われ、集合的にはワークフローとして知られています。 GitHub は、ワークフローの説明を探して .github/workflows ディレクトリの YAML ファイル (拡張子が .yml または .yaml のファイル) を検索します。 詳細は、GitHub Actions の主要コンセプトをご覧ください。

これから実行するアクションはすべて ZPM-registry リポジトリのフォーク内で実行されます。 この記事では、このフォークを "zpm-registry" と呼び、そのルートディレクトリーを "<root_repo_dir>" と呼びます。 ZPM アプリケーションそのものに関する詳細は、Introducing InterSystems ObjectScript Package Manager および The Anatomy of ZPM Module: Packaging Your InterSystems Solution をご覧ください。

すべてのコードサンプルは、簡単にコピー & ペーストできるよう、こちらのリポジトリに保管されています。 必須事項は、CircleCI ビルドで GKE の作成を自動化すると題した記事に記載されている内容と同じです。

過去の記事を読まれていること、Google アカウントを既にお持ちであること、そして前回の記事で「Development」と名付けたプロジェクトを作成されているという前提で進めていきます。 この記事では、その ID を <PROJECT_ID>として表示しています。 下の例を実行する場合は、ご自身のプロジェクトの ID に変更してください

Google には無料のティアがありますが、Google クラウドプラットフォームは有料ですのでご注意ください。 経費の管理にはお気を付けください。

ワークフローの基本

それでは、始めましょう。 

以下はシンプルですが使いものにならないワークフローの例です。

$ cd <root_repo_dir>
$ mkdir -p .github/workflows
$ cat <root_repo_dir>/.github/workflows/workflow.yaml          
name: Traditional Hello World
on: [push]
jobs:
  courtesy:
    name: Greeting
    runs-on: ubuntu-latest
    steps:
    - name: Hello world
      run: echo "Hello, world!

リポジトリにプッシュするときは、「Greeting」と名付けたジョブを実行する必要があります。このジョブは唯一のステップとしてウェルカムフレーズを出力します。 このジョブは、GitHub でホストされる仮想マシン「Runner」で実行されます。同マシンには Ubuntu の最新バージョンがインストールされています。
このファイルをリポジトリにプッシュした後に「Code GitHub」タブを開けば、すべて順調に実行されたことを確認できるはずです。

ジョブが失敗したら、緑のチェックマークの代わりに赤の「X」が表示されます。 詳細を見るには、緑のチェックマークと「Details」を順にクリックします。 もしくは、直接「Actions」タブに移動することもできます。

ワークフローの構文に関する完全な詳細は、Workflow syntax for GitHub Actions と題したヘルプドキュメントをご覧ください。

リポジトリにイメージビルド用の Dockerfile が含まれている場合は、「Hello world」のステップをもっと役に立つものに置き換えることができます。こちらは starter-workflows から選んだ 1 例です。

steps:
- uses: actions/checkout@v2
- name: Build the Docker image
  run: docker build . --file Dockerfile --tag my-image:$(date +%s)

新しいステップ "uses: action/checkout@v2" が追加されていることに注目してください。 "checkout" という名前からも、リポジトリをクローンするステップであるのは分かりますが、詳細はどこを見ればいいのか? 

CircleCI については、便利なステップがたくさんありますが、書き直す必要はありません。 その代わりに、Marketplace という共有リソースから取り入れることができます。 そこで実行したいアクションを探します。「By actions」とマーキングされているアクション (カーソルを合わせると、"Creator verified by Github" と表示されるアクション) をおすすめします。

ワークフローの "uses" 句には、独自のモジュールを記述する代わりに、既成のモジュールを使用する意図が示されています。

アクションの実装そのものは、どの言語でも記述できますが、JavaScript が推奨されます。 JavaScript (または TypeScript) で書かれたアクションは、直接 Runner のマシンで実行されます。 他のかたちで実装する場合は、ユーザーが指定する Docker コンテナが希望する内部環境で実行されますが、もちろんその速度は少し遅くなります。 アクションの詳細は、そのままのタイトルが付けられた記事 About actions をお読みください。 

checkout アクションは TypeScript で書かれています。 また、この記事の例で使う Terraform アクションは、Docker Alpine で起動される標準的な Bash シェルスクリプトです。

クローンしたリポジトリに Dockerfile がありますので、修得した知識を応用してみましょう。 ZPM レジストリのイメージをビルドし、Google Container Registry にプッシュします。 並行して、このイメージを実行する Kubernetes クラスターを作成します。これには Kubernetes のマニフェストを使用します。 

私たちの計画を GitHub が理解できる言語で記述すると以下のようになります (見やすくするために行をたくさん省いて俯瞰したものですので、このコンフィグは使わないでください)。

name: ワークフローの説明
# Trigger condition. In this case, only on push to ‘master’ branch
on:
  push:
    branches:
    - master

# ここではすべての後続のジョブとそれらの各ステップで 
# 使用可能な環境変数について説明しています
# これらの変数は GitHub Secrets ページで初期化できます
# それらを参照する目的で “${{ secrets }}” を追加しています
env:
  PROJECT_ID: ${{ secrets.PROJECT_ID }}

# ジョブリストの定義 ジョブ / ステップの名前はランダムなもので構いませんが
# 有意義なほうがベターです
jobs:
  gcloud-setup-and-build-and-publish-to-GCR:
    name: gcloud ユーティリティをセットアップし、ZPM イメージをビルドし、Container Registry に公開する
    runs-on: ubuntu-18.04
    steps:
    - name: チェックアウト
    - name: gcloud cli をセットアップする
    - name: gcloud を認証情報ヘルパーとして使用するよう Docker を設定する
    - name: ZPM イメージをビルドする
    - name: ZPM イメージを Google Container Registry に公開する

  gke-provisioner:
    name: Provision GKE cluster
    runs-on: ubuntu-18.04
    steps:
    - name: チェックアウト
    - name: Terraform 初期化
    - name: Terraform 検証
    - name: Terraform プラン
    - name: Terraform 適用

  kubernetes-deploy:
    name: Kubernetes マニフェストを GKE クラスタにデプロイする
    needs:
    - gcloud-setup-and-build-and-publish-to-GCR
    - gke-provisioner
    runs-on: ubuntu-18.04
    steps:
    - name: チェックアウト
    - name: プレースホルダーをステートフルセットのテンプレートに記載の値に置換する
    - name: gcloud cli をセットアップする
    - name: Kubernetes のマニフェストを適用する

これが実用的な config のスケルトン (骨格だけのコード) で、その筋肉、つまり各ステップで実行されるアクションは含まれていません。 アクションはシンプルなコンソールコマンドで実行できます ("run"、複数のコマンドがあれば "run |")。         

- name: gcloud を認証情報ヘルパーとして使用するよう Docker を設定する
  run: |
    gcloud auth configure-docker

アクションは "uses" を使ってモジュールとして起動することもできます。

- name: チェックアウト
  uses: actions/checkout@v2

デフォルトでは、すべてのジョブが並列して実行され、各ステップが順番に処理されます。 しかし、"needs" を使えば、そのジョブを残りのジョブが完了するまで待機するジョブとして指定できます。

needs:
- gcloud-setup-and-build-and-publish-to-GCR
- gke-provisioner

ちなみに、このように待機するジョブが GitHub の Web インターフェイスに表示されるのは、待機される側のジョブが実行されるときだけです。

"gke-provisioner" ジョブには過去の記事の中で検証した Terraform が記述されています。 GCP 環境で操作する場合の事前設定は、便宜上、別の markdown file で繰り返されます。 以下のリンクもご利用いただくと便利です。

  • Terraform Apply Subcommand documentation
  • Terraform GitHub Actions repository
  • Terraform GitHub Actions documentation
  • "kubernetes-deploy" ジョブには、"Apply Kubernetes manifests" と呼ばれるステップがあります。 今回は、Deploying InterSystems IRIS Solution into GCP Kubernetes Cluster GKE Using CircleCI という記事に記載されている通りにマニフェストを使用しますが、少しだけ変更を加えます。

    過去の記事で使った IRIS アプリケーションは、ステートレスでした。 つまり、ポッドを再起動したら、すべてのデータがデフォルトの場所に戻っていたのです。 これは良いことで、必要になることも多々あります。しかし、ZPM レジストリでは、ポッドを何回再起動する必要があっても、読み込まれたパッケージを何とかして保存する必要があります。 デプロイすれば出来るのですが、もちろんそれには制限があります。 

    ステートフルなアプリケーションには、StatefulSet のリソースを選択する方が無難です。 メリットとデメリットは、Deployments vs. StatefulSets の GKE ドキュメンテーションに関するトピックおよび Kubernetes Persistent Volumes with Deployment and StatefulSet と題したブログ記事に記載しています。

    StatefulSet のリソースは、リポジトリの中にあります。 注目したいのは、以下の部分です。

    volumeClaimTemplates:
    - metadata:
        name: zpm-registry-volume
        namespace: iris
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 10Gi

    このコードは、単一の Kubernetes ワーカーノードによってマウント可能な 10GB の読み取り / 書き込みディスクを作成します。 このディスク (およびその中のデータ) はアプリケーションの再起動後も残ります。 StatefulSet 全体を削除しても残ります。そのためには正しい Reclaim Policy を設定する必要がありますが、この記事ではカバーしていません。

    ワークフローを起動させる前に、GitHub Secrets に変数をあといくつか追加しておきましょう。

    以下のテーブルはこれらの設定の意味を説明するものです (サービスアカウントキー も含まれています):

    名前意味
     GCR_LOCATION グローバル GCR ロケーション eu.gcr.io
     GKE_CLUSTER GKE クラスタ名 dev-cluster
     GKE_ZONE イメージを格納するゾーン europe-west1-b
     IMAGE_NAME イメージのレジストリ名 zpm-registry
     PROJECT_ID GCP プロジェクト ID possible-symbol-254507
     SERVICE_ACCOUNT_KEY GitHub が GCP に接続する際に使用する JSON key。 重要: Base64 でエンコードされている必要があります (下の注意点をお読みください) ewogICJ0eXB...
     TF_SERVICE_ACCOUNT_KEY Terraform が GCP に接続する際に使用する JSON key (下の注意点をお読みください) {…}

    SERVICE_ACCOUNT_KEY において、JSON-key に、例えば、key.json のような名前が付いている場合は、下のコマンドを実行します。

    $ base64 key.json | tr -d '\n'

    TF_SERVICE_ACCOUNT_KEY について、その権限は CircleCI ビルドで GKE の作成を自動化すると題した記事にて説明してあります。

    SERVICE_ACCOUNT_KEY のちょっとした注意点: 私がやってしまったように、base64 フォーマットに変換するのを忘れてしまうと、以下のような画面が表示されます。

    ワークフローの主要部分を確認し、必要な変数を追加したところで、ワークフローの完全版を検証する準備が整いました (<root_repo_dir>/.github/workflow/workflow.yaml)。

    name: ZPM レジストリのイメージを構築し、GCR にデプロイする。 GKE を実行。 ZPM レジストリを GKE で実行する。
    on:
      push:
        branches:
        - master

    # 環境変数。
    # ${{ secrets }} は GitHub -> Settings -> Secrets より取得されます
    # ${{ github.sha }} はコミットハッシュです
    env:
      PROJECT_ID: ${{ secrets.PROJECT_ID }}
      SERVICE_ACCOUNT_KEY: ${{ secrets.SERVICE_ACCOUNT_KEY }}
      GOOGLE_CREDENTIALS: ${{ secrets.TF_SERVICE_ACCOUNT_KEY }}
      GITHUB_SHA: ${{ github.sha }}
      GCR_LOCATION: ${{ secrets.GCR_LOCATION }}
      IMAGE_NAME: ${{ secrets.IMAGE_NAME }}
      GKE_CLUSTER: ${{ secrets.GKE_CLUSTER }}
      GKE_ZONE: ${{ secrets.GKE_ZONE }}
      K8S_NAMESPACE: iris
      STATEFULSET_NAME: zpm-registry

    jobs:
      gcloud-setup-and-build-and-publish-to-GCR:
        name: gcloud ユーティリティをセットアップ、ZPM イメージを構築、Container Registry に公開する
        runs-on: ubuntu-18.04
        steps:
        - name: チェックアウト
          uses: actions/checkout@v2

        - name: gcloud cli をセットアップする
          uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
          with:
            version: '275.0.0'
            service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }}

        - name: gcloud を認証情報ヘルパーとして使用するよう Docker を設定する
          run: |
            gcloud auth configure-docker

        - name: ZPM イメージを構築する
          run: |
            docker build -t ${GCR_LOCATION}/${PROJECT_ID}/${IMAGE_NAME}:${GITHUB_SHA} .

        - name: ZPM イメージを Google Container Registry に公開する
          run: |
            docker push ${GCR_LOCATION}/${PROJECT_ID}/${IMAGE_NAME}:${GITHUB_SHA}

      gke-provisioner:
      # Inspired by:
      ## https://www.terraform.io/docs/github-actions/getting-started.html
      ## https://github.com/hashicorp/terraform-github-actions
        name: GKE クラスタをプロビジョンする
        runs-on: ubuntu-18.04
        steps:
        - name: チェックアウト
          uses: actions/checkout@v2

        - name: Terraform 初期化
          uses: hashicorp/terraform-github-actions@master
          with:
            tf_actions_version: 0.12.17
            tf_actions_subcommand: 'init'
            tf_actions_working_dir: 'terraform'

        - name: Terraform 検証
          uses: hashicorp/terraform-github-actions@master
          with:
            tf_actions_version: 0.12.17
            tf_actions_subcommand: 'validate'
            tf_actions_working_dir: 'terraform'

        - name: Terraform プラン
          uses: hashicorp/terraform-github-actions@master
          with:
            tf_actions_version: 0.12.17
            tf_actions_subcommand: 'plan'
            tf_actions_working_dir: 'terraform'

        - name: Terraform 適用
          uses: hashicorp/terraform-github-actions@master
          with:
            tf_actions_version: 0.12.17
            tf_actions_subcommand: 'apply'
            tf_actions_working_dir: 'terraform'

      kubernetes-deploy:
        name: Kubernetes マニフェストを GKE クラスタにデプロイする
        needs:
        - gcloud-setup-and-build-and-publish-to-GCR
        - gke-provisioner
        runs-on: ubuntu-18.04
        steps:
        - name: チェックアウト
          uses: actions/checkout@v2

        - name: プレースホルダーをステートフルセットのテンプレートに記載の値に置換する
          working-directory: ./k8s/
          run: |
            cat statefulset.tpl |\
            sed "s|DOCKER_REPO_NAME|${GCR_LOCATION}/${PROJECT_ID}/${IMAGE_NAME}|" |\
            sed "s|DOCKER_IMAGE_TAG|${GITHUB_SHA}|" > statefulset.yaml
            cat statefulset.yaml

        - name: gcloud cli をセットアップする
          uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
          with:
            version: '275.0.0'
            service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }}

        - name: Kubernetes マニフェストを適用する
          working-directory: ./k8s/
          run: |
            gcloud container clusters get-credentials ${GKE_CLUSTER} --zone ${GKE_ZONE} --project ${PROJECT_ID}
            kubectl apply -f namespace.yaml
            kubectl apply -f service.yaml
            kubectl apply -f statefulset.yaml
            kubectl -n ${K8S_NAMESPACE} rollout status statefulset/${STATEFULSET_NAME}

    リポジトリにプッシュする前には、terraform-code を github-gke-zpm-registry リポジトリの Terraform ディレクトリーから取得し、プレースホルダーを main.tf のコメントに記載されている通りに置換し、terraform/ ディレクトリーに配置する必要があります。 Terraform は、リモートのバケットを使用しますが、このバケットは最初から CircleCI ビルドで GKE の作成を自動化すると題した記事で言及されている通りに作成されている必要があることを覚えておきましょう。

    また、Kubernetes-code は github-gke-zpm-registry リポジトリの K8S ディレクトリーから取得され、k8s/ ディレクトリーの中に配置されている必要があります。 これらのコードのソースは、スペースを節約するためにこの記事では省いています。 

    そして、以下のようにすればデプロイをトリガーできます。

    $ cd <root_repo_dir>/
    $ git add .github/workflow/workflow.yaml k8s/ terraform/
    $ git commit -m “Add GitHub Actions deploy”
    $ git push

    フォークされている ZPM リポジトリに変更内容をプッシュしたら、説明したステップの実装を確認できます。
     

    ここまで、ジョブの数は 2 つしかありませんが、 3 つ目の "kubernetes-deploy" は、その 2 つのジョブに依存しており、それらが完了した後に表示されます。
    Docket イメージのビルドと公開には少し時間がかかります。


    また、結果は GCR コンソールで確認できます。

    "Provision GKE cluster" ジョブは、最初の実行時に GKE クラスターを作成するので、最初だけ完了までの時間が少し長くなります。 数分間、待機中の画面が表示されます。

    やっと完了したときには、思わず嬉しくなります。

    Kubernetes のリソースも喜んでいます。

    $ gcloud container clusters get-credentials <CLUSTER_NAME> --zone <GKE_ZONE> --project <PROJECT_ID>

    $ kubectl get nodes
    NAME                                                                                                   STATUS ROLES   AGE        VERSION
    gke-dev-cluster-dev-cluster-node-pool-98cef283-dfq2 Ready    <none> 8m51s   v1.13.11-gke.23

    $ kubectl -n iris get po
    NAME                     READY   STATUS     RESTARTS   AGE
    zpm-registry-0   1/1         Running    0                      8m25s

    他の内容の確認は、Running ステータスになるまで待機することをおすすめします。

    $ kubectl -n iris get sts
    NAME                 READY   AGE
    zpm-registry   1/1          8m25s

    $ kubectl -n iris get svc
    NAME                 TYPE                      CLUSTER-IP       EXTERNAL-IP    PORT(S)                       AGE
    zpm-registry   LoadBalancer    10.23.248.234   104.199.6.32     52773:32725/TCP   8m29s

    ディスクまでが喜んでいます。

    $ kubectl get pv -oyaml | grep pdName
      pdName: gke-dev-cluster-5fe434-pvc-5db4f5ed-4055-11ea-a6ab-42010af00286

    そして、一番喜んでいるのは ZPM レジストリです ("kubectl -n iris get svc" の External-IP 出力を使用しました):

    $ curl -u _system:SYS 104.199.6.32:52773/registry/_ping
    {"message":"ping"}

    ログイン / パスワードを HTTP で処理しているのは残念ですね。今後の記事の中で何とかしたいと思います。

    ちなみにですが、エンドポイントについてはソースコードを見ていただければ、詳細が書かれていますので、XData UrlMap セクションをご覧ください。

    このリポジトリは、それ自体にパッケージをプッシュすればテストできます。 GitHub のダイレクトリンクだけをプッシュする便利な機能があります。 InterSystems ObjectScript の数式ライブラリで試してみましょう。 これをローカルマシンから実行します。

    $ curl -XGET -u _system:SYS 104.199.6.32:52773/registry/packages/-/all
    []
    $ curl -i -XPOST -u _system:SYS -H "Content-Type: application/json" -d '{"repository":"https://github.com/psteiwer/ObjectScript-Math"}' 'http://104.199.6.32:52773/registry/package'
    HTTP/1.1 200 OK
    $ curl -XGET -u _system:SYS 104.199.6.32:52773/registry/packages/-/all
    [{"name":"objectscript-math","versions":["0.0.4"]}]

    ポッドを再起動して、データがきちんと配置されていることを確認します。

    $ kubectl -n iris scale --replicas=0 sts zpm-registry
    $ kubectl -n iris scale --replicas=1 sts zpm-registry
    $ kubectl -n iris get po -w

    実行中のポッドを待ちます。 そして、うまく処理されると以下が表示されます。

    $ curl -XGET -u _system:SYS 104.199.6.32:52773/registry/packages/-/all
    [{"name":"objectscript-math","versions":["0.0.4"]}]

    それでは、この数式パッケージをリポジトリからローカルの IRIS インスタンスにインストールしましょう。 ZPM クライアントが既にインストールされているものを選びます。

    $ docker exec -it $(docker run -d intersystemsdc/iris-community:2019.4.0.383.0-zpm) bash
    $ iris session iris
    USER>write ##class(Math.Math).Factorial(5)
    <CLASS DOES NOT EXIST> *Math.Math
    USER>zpm
    zpm: USER>list
    zpm: USER>repo -list
    registry
        Source:     https://pm.community.intersystems.com
        Enabled?    Yes
        Available?    Yes
        Use for Snapshots?    Yes
        Use for Prereleases?    Yes
    zpm: USER>repo -n registry -r -url http://104.199.6.32:52773/registry/ -user _system -pass SYS
    zpm: USER>repo -list                                                                          
    registry
        Source:     http://104.199.6.32:52773/registry/
        Enabled?    Yes
        Available?    Yes
        Use for Snapshots?    Yes
        Use for Prereleases?    Yes
        Username:     _system
        Password:     <set>
    zpm: USER>repo -list-modules -n registry
    objectscript-math 0.0.4
    zpm: USER>install objectscript-math
    [objectscript-math]    Reload START
    ...
    [objectscript-math]    Activate SUCCESS


    zpm: USER>quit
    USER>write ##class(Math.Math).Factorial(5)                                               
    120

    おめでとうございます!
    いらなくなった GKE クラスタは、忘れずに削除しておきましょう。
     

    まとめ

    InterSystems のコミュニティには GitHub Actions のリファレンスがそれほど多くありません。 見つかったのは、エキスパート @mdaimorメンション 1 つのみです。 ですが、GitHub Actions はコードを GitHub に保管するディベロッパーにとって非常に重宝すると思われます。 ネイティブアクションは JavaScript でしかサポートされていませんが、これはディベロッパーの多くがコードを使ってステップを説明することに慣れており、そうすることが望ましいということかもしれません。 いずれにしても、JavaScript に詳しくない方は Docker アクションを使えばいいと思います。

    GitHub Actions の UI に関して、使っているうちに不便に感じたことがいくつかあり、知っておくべきだと思うことを紹介しておきます。

  • ジョブのステップが完了するまで、その状況を確認できない。 "Terraform apply" のステップのように、クリックすることができない。
  • 失敗したワークフローは再実行できる一方で、成功したワークフローを再実行する方法が見つからなかった。
  • 2 つ目のポイントの次善策として、次のコマンドを使います。

    $ git commit --allow-empty -m "trigger GitHub actions" 

    これに関する詳細は、StackOverflow に投稿されている How do I re-run Github Actions? (Github Actions はどうすれば再実行できるか?) という質問をご覧ください。

    0
    0 491
    記事 Shintaro Kaminaka · 11月 27, 2020 5m read

    背景

    InterSystems IRIS 2019 では、新たに魅力的な機能が導入される予定です。 ぜひ知っておくべき魅力的な新機能の一つには、API 管理があります。

    OpenAPI Initiative(https://www.openapis.org/)は、API を定義するための標準仕様(https://github.com/OAI/OpenAPI-Specification)をサポートする組織です。 OpenAPI 仕様(OAS)は、REST API 向けのプログラミング言語に依存しない標準的なインターフェースの記述を定義するもので、人間とコンピューターの両方が、ソースコードへのアクセス、追加ドキュメント、またはネットワークトラフィックの検査を必要とせずに、サービスの機能を検出して理解できるようにしています。 OpenAPI を使用して適切に定義されている場合、消費者は最小限の実装ロジックでリモートサービスを理解して対話できます。 低レベルのプログラミングに対するインターフェース記述と同様に、OpenAPI 仕様によってサービスを呼び出す際の当て推量が排除されます。

    InterSystems は InterSystems IRIS で API 設計優先のアプローチをサポートしており、それによって先に仕様を設計してからサーバーサイドを生成できるようにしています。 API を先に設計する場合、通常は Swagger Editor やその他同様のツールを使用して仕様を作成し、必要に応じて JSON 形式で OAS 仕様を取得します。

    API を設計して実装する準備ができたら、OAS 仕様を使用してサーバーサイドの API ロジックを作成できるようになります。 InterSystems IRIS 2019.1 では、新しいルーチンである ^%REST を使用し API を土台にして、ビジネスロジックを呼び出すコードを配置するクラスを自動的に生成できます。 このクラスのメソッドは命名規則に基づいて生成されますが、メソッドとクラスを仕様(operationId)で定義することもできます。

    InterSystems IRIS REST コマンドラインインターフェースの使用例を以下に示します。

    USER>do ^%REST
     
    REST Command Line Interface (CLI) helps you CREATE or DELETE a REST application 
    Enter an application name or (L)ist all REST applications (L): acmeapi
    REST application not found: acmeapi
    Do you want to create a new REST application? Y or N (Y):
     
    File path or absolute URL of a swagger document.
    If no document specified, then create an empty application.
    OpenAPI 2.0 swagger: C:\myspec\acme.swagger.json
     
    OpenAPI 2.0 swagger document: C:\myspec\notification.swagger.json
    Confirm operation, Y or N (Y):
    -----Creating REST application: acmeapi-----
    CREATE acmeapi.spec
    GENERATE acmeapi.disp
    CREATE acmenapi.impl
    REST application successfully created.
     
    Create a web application for the REST application? Y or N (Y):
    Specify web application name. Default is /csp/api/acme
    Web application name: /csp/api/acme/v1
     
    -----Deploying REST application: acmeapi-----
    Application acmeapi deployed to /csp/api/acme/v1
     

    現時点では、REST API を作成する際に API の土台の構築に使用できるのは OpenAPI 2.0 Swagger 仕様だけです。

    ご覧のとおり、このルーチンは以下の 3 つのクラスを作成します。

    • .spec: このクラスは swagger 仕様(XData OpenAPI ブロック)のコンテナです。 このクラスは読み取り専用です。
    • .disp: CSP アプリケーションで使用できるディスパッチクラスです。 %CSP.REST を拡張し、XData UrlMap を定義するものです。 また、このクラスは読み取り専用であり、システムクラスとして扱われます(デフォルトでは Atelier で非表示になっています)。
    • .impl: 必要なすべての署名メソッドを定義するクラスです。 API を機能させるには、このクラスを完成させる必要があります。

    すでに開発済みの API がある場合

    InterSystems は、開発者がリモートで API 機能を探索できるサービス検索機能を InterSystems IRIS 2018.1 で導入しました。 また、Swagger を統合することで、既存の REST アプリケーションから Open API 仕様(OAS)を生成できるようにしています。 そのため、InterSystems IRIS で変更される API はすべて swagger 仕様を自動生成できます。

    次のように管理 API を使用すると、システムで使用可能なすべての API を照会することができます。

    HTTP GET http://:/api/mgmnt/ 

    次の結果が返ってきます。


    [
    ...,
        {
            "name": "/csp/petstore/v2",
            "dispatchClass": "petstore.disp",
            "namespace": "USER",
            "resource": "",
            "swaggerSpec": "/api/mgmnt/v1/USER/spec/csp/petstore/v2",
            "enabled": true
        }
    ]

    また、API の Swagger 仕様は swaggerSpec プロパティによって示された URL に HTTP GET を発行することで取得できます。 元の swagger 仕様で定義されている API 操作には、アクションを実装する必要があるメソッドの名前を定義する新しいプロパティが追加されています。

    例:

    "x-ISC_ServiceMethod": "getPetById",

    この api/mgmnt は検索だけではなく、次のように API の作成/照会/削除にも使用できるのが非常に魅力的です。

    HTTP POST to /api/mgmnt/v2//
    HTTP GET to /api/mgmnt/v2//
    HTTP DELETE to /api/mgmnt/v2//

    IRIS API Explorer

    IRIS Explorer はこの API を利用し、IRIS API を非常にわかりやすく視覚化して管理するためのツールを提供する Angular 5 アプリケーションです。 以下、簡単にご紹介します。

    まず、InterSystems IRIS インスタンスにログインする必要があります(デフォルトでは、52773 番ポートでローカルインスタンスを探します)。

    ログイン後、アプリがクエリを実行して既存のすべての API を取得します。

    ここでは既存の API を削除することも、新しい API を作成することもできます。 新しいアプリケーションを作成するには、ネームスペース / アプリケーション名 / Swagger 仕様を含む .json ファイルを指定する必要があります。

    API を作成したら、仕様を表示できるようになります。 私はさらにこれを見やすくするため、Swager-UI(https://github.com/swagger-api/swagger-ui)を埋め込みました。

    もちろん、JSON 形式の OSA 仕様も取得できます。

    すべてのコードはオープンであり、都合に合わせて使用または変更することができます。 このアプリは Open Exchange で入手できます。

    https://openexchange.intersystems.com/package/iris-explorer

    GitHub でも公開しています。

    https://github.com/drechema/iris-explorer

    お役に立てば幸いです。

    0
    0 314
    記事 Shintaro Kaminaka · 9月 11, 2020 8m read

    開発者の皆さん、こんにちは。

    今回の記事では前回の記事に引き続き、IRIS for Health上で、FHIRリポジトリ+OAuth2認可サーバ/リソースサーバを構成する方法をご案内します。

    (注意:2020.4以降のバージョンではこの記事に記載されているスコープ指定では正しくリソースが取得できません。詳細はこちらの記事をご覧ください。)

    パート1では、事前準備と、OAuth2認可サーバを構成し、アクセストークンを取得するとこまでをご紹介しました。
    このパート2では、FHIRリポジトリの構築方法と、OAuth2クライアント/リソースサーバの構成方法をご紹介していきます。

    今日構成する、FHIRリポジトリおよび、OAuth2クライアント/リソースサーバの構成は、前回パート1で構成したOAuth2認可サーバのIRISインスタンスと分けることもできますし、同じインスタンスに同居させることもできます。
    この記事の中では前回と同じインスタンス上に構成していきます。

    FHIRリポジトリの構築とOAuth Client Nameの指定

    FHIRリポジトリの構築方法は、過去の記事「Azure上でIRIS for Healthをデプロイし、FHIR リポジトリを構築する方法」で紹介しています。

    5
    0 794
    記事 Shintaro Kaminaka · 9月 4, 2020 13m read

    開発者の皆さん、こんにちは。

    今回の記事ではFHIRと組み合わせて使用されるケースが増えてきている、権限の認可(Authorization)を行うためのOAuth2について取り上げます。

    まずこのパート1では、IRIS for HealthおよびApacheのDockerコンテナの起動と、IRIS for Health上で、OAuth2認可サーバ機能を構成し、REST開発ツールPostmanからアクセスし、アクセストークンを取得する方法について解説します。
    さらにパート2以降では、IRIS for HealthにFHIRリポジトリ機能を追加し、OAuth2リソースサーバ構成を追加して、Postmanからアクセストークンを使用したFHIRリクエストの実行方法まで解説します。

    InterSystems製品のOAuth2機能の解説については、すでにいくつかの記事が開発者コミュニティ上に公開されていますが、今回は改めて最新バージョンでの構成方法を解説したいと思います。
    InterSystems IRIS Open Authorization Framework(OAuth 2.0)の実装 - パート1

    1
    0 1436
    記事 Shintaro Kaminaka · 8月 26, 2020 23m read

    作成者:Daniel Kutac(InterSystems セールスエンジニア)

    パート 3. 付録

    InterSystems IRIS OAUTH クラスの説明

    この連載の前のパートでは、InterSystems IRIS を OAUTH クライアントおよび認可/認証サーバー(OpenID Connect を使用)として機能するように構成する方法について学びました。 この連載の最後のパートでは、InterSystems IRIS OAuth 2.0 フレームワークを実装するクラスについて説明します。 また、一部の API クラスのメソッドの使用例についても説明します。

    OAuth 2.0 を実装する API クラスは、目的に応じて 3 種類のグループに分けることができます。 すべてのクラスは %SYS ネームスペースで実装されています。 これらの一部は(% package 経由で)公開されていますが、一部は非公開になっており、開発者が直接呼び出すことはできません。

    内部クラス

    これらのクラスは OAuth2 パッケージに属しています。

    次の表に、対象となるクラスの一部を掲載しています(クラスの完全なリストについては、CachéあるいはIRIS インスタンスのオンラインクラスリファレンスを参照してください)。 以下の表に掲載されているものを除き、これらのクラスをアプリケーション開発者が直接使用することはできません。

      <td>
        説明
      </td>
    </tr>
    
    <tr style="height:0px">
      <td>
        OAuth2.AccessToken
      </td>
      
      <td>
        Persistent(永続クラス)
    

    OAuth2.AccessToken は、OAuth 2.0 アクセストークンとその関連情報を格納します。 これは、アクセストークンの OAUTH クライアントコピーです。OAuth2.AccessToken は、SessionId と ApplicationName の組み合わせによってインデックス化されます。 したがって、SessionId/ApplicationName ごとに 1 つのスコープのみをリクエストできます。 2 回目のリクエストが別のスコープで行われ、アクセストークンがまだ付与されている場合は、新しいリクエストのスコープが予期されるスコープになります。

    <tr style="height:0px">
      <td>
        OAuth2.Client
      </td>
      
      <td>
        Persistent(永続クラス)
    

    OAuth2.Application クラスは OAuth2 クライアントを記述し、RFC 6749 に基づいてアプリケーションを認可するために使用する認可サーバーを参照します。 クライアントシステムは、さまざまなアプリケーションで複数の認可サーバーと共に使用できます。

    <tr style="height:0px">
      <td>
        OAuth2.Response
      </td>
      
      <td>
        CSPページ
    

    これは、InterSystems IRIS OAuth 2.0 クライアントコードから使用される OAuth 2.0 認可サーバーからの応答用のランディングページです。 応答はここで処理され、最終的なターゲットにリダイレクトされます。

    <tr style="height:0px">
      <td>
        OAuth2.ServerDefinition
      </td>
      
      <td>
        Persistent(永続クラス)
    

    OAUTH クライアント(この InterSystems IRIS インスタンス)が使用する認可サーバーの情報が格納されています。 認可サーバーの定義ごとに複数のクライアント構成を定義できます。

    <tr style="height:0px">
      <td>
        OAuth2.Server.AccessToken
      </td>
      
      <td>
        Persistent(永続クラス)
    

    アクセストークンは OAUTH サーバーの OAuth2.Server.AccessToken によって管理されます。 このクラスには、アクセストークンと関連プロパティが格納されます。 このクラスは、認可サーバーのさまざまな要素間の通信手段でもあります。

    <tr style="height:0px">
      <td>
        OAuth2.Server.Auth
      </td>
      
      <td>
        CSP ページ
    

    認可サーバーは、RFC 6749 で指定されている認可コードおよびインプリシットグラントタイプの認可制御フローをサポートします。 OAuth2.Server.Auth クラスは認可エンドポイントとして機能し、RFC 6749 に従ってフローを制御する %CSP.Page のサブクラスです。

    <tr style="height:0px">
      <td>
        OAuth2.Server.Client
      </td>
      
      <td>
        Persistent(永続クラス)
    

    OAuth2.Server.Configuration は、この認可サーバーに登録したクライアントを記述する永続クラスです。

    <tr style="height:0px">
      <td>
        OAuth2.Server.Configuration
      </td>
      
      <td>
        Persistent(永続クラス)
    

    認可サーバーの構成が格納されます。 すべての構成クラスには、ユーザーが構成の詳細を入力するためのシステム管理ポータルページがそれぞれ存在します。

    クラス名

    OAuth2.Client、OAuth2.ServerDefinition、OAuth2.Server.Client、OAuth2.Configuration の各オブジェクトは UI を使用せずに開き、変更し、作成または変更した構成を保存できます。 これらのクラスを使用し、構成をプログラムで操作できます。

    サーバーカスタマイズ用クラス

    これらのクラスは %OAuth2 パッケージに属しています。 このパッケージには、一連の内部クラス(ユーティリティ)が含まれています。ここでは、開発者が使用できるクラスについてのみ説明します。 これらのクラスは、OAuth 2.0 Server Configuration(サーバー構成)ページで参照されます。

      <td>
        CSPページ
    

    %OAuth2.Server.Authenticate はデフォルトの Authenticate クラスだけでなく、ユーザーが作成したすべての Authenticate クラスのサブクラスとして機能します。 Authenticate クラスは、ユーザーを認証するために OAuth2.Server.Auth の認可エンドポイントによって使用されます。 このクラスを使用すると、認証プロセスをカスタマイズできます。次のメソッドを OAuth2.Server のデフォルトをオーバーライドするために実装できます。 DirectLogin– ログインページを表示しない場合にのみ使用します。 DisplayLogin – 認可サーバーのログインフォームを実装します。 DisplayPermissions – リクエストされたスコープのリストを使用してフォームを実装します。CSS を変更することで、外観や操作性をさらにカスタマイズできます。 CSS スタイルは DrawStyle メソッドで定義されます。loginForm は DisplayLogin フォーム用です。permissionForm は DisplayPermissions フォーム用です。

    <tr style="height:0px">
      <td>
        %OAuth2.Server.Validate
      </td>
      
      <td>
        CSP ページ
    

    これは、サーバーに含まれているデフォルトの Validate User Class(ユーザー検証クラス)です。 デフォルトのクラスは、認証サーバーが配置されている Cache またはIRISインスタンスのユーザーデータベースを使用してユーザーを検証します。 サポートされるプロパティは、issuer(Issuer)、role、sub(Username)です。Validate User Class は Authorization Server Configuration(認可サーバーの構成)で指定されます。 ユーザー名とパスワードの組み合わせを検証し、ユーザーに関連付けられたプロパティ一式を返す ValidateUser メソッドを含める必要があります。

    <tr style="height:0px">
      <td>
        %OAuth2.Server.Generate
      </td>
      
      <td>
        Registered Object(登録オブジェクト)
    

    %OAuth2.Server.Generate は、サーバーに含まれているデフォルトのGenerate Token Class(トークン生成クラス)です。 デフォルトのクラスは、ランダムな文字列を opaque アクセストークンとして生成します。Generate Token Class は、Authorization Server Configuration で指定されます。 ValidateUser メソッドによって返されるプロパティの配列に基づいてアクセストークンを生成するために使用される GenerateAccessToken メソッドを含める必要があります。

    <tr style="height:0px">
      <td>
        %OAuth2.Server.JWT
      </td>
      
      <td>
        Registered Object(登録オブジェクト)
    

    %OAuth2.Server.JWT は、サーバーに含まれている JSON Web トークンを作成する Generate Token Class です。 Generate Token Class は、Authorization Server Configuration で指定されます。 ValidateUser メソッドによって返されるプロパティの配列に基づいてアクセストークンを生成するために使用される GenerateAccessToken メソッドを含める必要があります。

    <tr style="height:0px">
      <td>
        %OAuth2.Utils
      </td>
      
      <td>
        Registered Object(登録オブジェクト)
    

    このクラスは、さまざまなエンティティのログの記録を実装します。 カスタマイズの章のサンプルコードに、可能な使用法を示しています。

    %OAuth2.Server.Authenticate

    次の画像に、OAuth 2.0 認可サーバー構成の対応するセクションを示します。

    OpenID Connect を JWT 形式の識別情報トークン(id_token)と共に使用する場合は、構成内のデフォルトの Generate Token Class である %OAuth2.Server.Generate%OAuth2.Server.JWT に置換してください。それ以外の場合は、デフォルトの Generate クラスのままにしてください。

    カスタマイズオプションについては、後で別の章で詳しく説明します。

    公開 API クラス

    公開 API クラスは、アプリケーション開発者が Web アプリケーションのメッセージフローに正しい値を渡し、アクセストークンの検証やイントロスペクションなどを実行するために使用されます。

    これらのクラスは %SYS.OAuth2 パッケージで実装されています。 次の表に、実装されているクラスの一部を掲載しています。

      <td>
        Registered Object(登録オブジェクト)
    

    %SYS.OAuth2.AccessToken クラスは、リソースサーバーへの認証にアクセストークンを使用できるようにするクライアント操作を定義します。基本となるトークンは、CACHESYS データベースの OAuth2.AccessToken に格納されます。 OAuth2.AccessToken は、SessionId と ApplicationName の組み合わせによってインデックス化されます。 したがって、SessionId/ApplicationName ごとに 1 つのスコープのみをリクエストできます。 2 回目のリクエストが別のスコープで行われ、アクセストークンがまだ付与されている場合は、新しいリクエストのスコープが予期されるスコープになります。

    <tr style="height:0px">
      <td>
        %SYS.OAuth2.Authorization
      </td>
      
      <td>
        Registered Object(登録オブジェクト)
    

    %SYS.OAuth2.Authorization クラスには、アクセストークンを取得してクライアントを認可するために使用される操作が含まれています。基本となるトークンは、CACHESYS データベースの OAuth2.AccessToken に格納されます。 OAuth2.AccessToken は、SessionId と ApplicationName の組み合わせによってインデックス化されます。 したがって、SessionId/ApplicationName ごとに 1 つのスコープのみをリクエストできます。 2 回目のリクエストが別のスコープで行われ、アクセストークンがまだ付与されている場合は、新しいリクエストのスコープが予期されるスコープになります。このクラスは CACHELIB にあるため、どこでも使用できることに注意してください。 ただし、トークンストレージは CACHESYS にあるため、ほとんどのコードでは直接使用できません。

    <tr style="height:0px">
      <td>
        %SYS.OAuth2.Validation
      </td>
      
      <td>
        Registered Object(登録オブジェクト)
    

    %SYS.OAuth2.Validation クラスは、アクセストークンの検証(または無効化)に使用されるメソッドを定義します。 基本となるトークンは、CACHESYS データベースの OAuth2.AccessToken に格納されます。 OAuth2.AccessToken は、SessionId と ApplicationName の組み合わせによってインデックス化されます。 したがって、SessionId/ApplicationName ごとに 1 つのスコープのみをリクエストできます。 2 回目のリクエストが別のスコープで行われ、アクセストークンがまだ付与されている場合は、新しいリクエストのスコープが予期されるスコープになります。

    %SYS.OAuth2.AccessToken

    このグループのいくつかのメソッドとクラスを詳しく見てみましょう。

    アクセストークンを使用するすべてのクライアントアプリケーションクラスは、その有効性をチェックする必要があります。 このチェックは、OnPage メソッド(または ZEN か ZENMojo ページの対応するメソッド)のどこかで実行されます。

    こちらがそのコードスニペットです。

     // OAuth2 サーバーからのアクセストークンがあるかどうかをチェックします。
     set isAuthorized=##class(%SYS.OAuth2.AccessToken).IsAuthorized(..#OAUTH2APPNAME,,"scope1,
         scope2",.accessToken,.idtoken,.responseProperties,.error)
    
     // アクセストークンがあるかどうかをさらにチェックします。
     // 以下は実行可能なすべてのテストであり、あらゆるケースで必要なわけではありません。
     // 各テストで返される JSON オブジェクトが単に表示されています。
     if isAuthorized {
        // 何らかの処理を実行します – リソースサーバーの API を呼び出して目的のデータを取得します。
     }
    

    リソースサーバーの API を呼び出すたびに、アクセストークンを提供する必要があります。 この処理は、%SYS.OAuth2.AccessToken メソッドの AddAccessToken メソッドによって実行されます。こちらがそのコードスニペットです。

     set httpRequest=##class(%Net.HttpRequest).%New()
      // AddAccessToken は現在のアクセストークンをリクエストに追加します。
      set sc=##class(%SYS.OAuth2.AccessToken).AddAccessToken(
        httpRequest,,
        ..#SSLCONFIG,
        ..#OAUTH2APPNAME)
     if $$$ISOK(sc) {
        set sc=httpRequest.Get(.. Service API url …)
     }
    

    この連載の前のパートで提供したサンプルコードでは、最初のアプリケーションページ(Cache1N)の OnPreHTTP メソッドでこのコードを確認することができました。 このコードは、アプリケーションの最初のページでアクセストークンチェックを実行するのに最適な場所です。

    ClassMethod OnPreHTTP() As %Boolean [ ServerOnly = 1 ]
    {
     set scope="openid profile scope1 scope2"
        #dim %response as %CSP.Response
     if ##class(%SYS.OAuth2.AccessToken).IsAuthorized(..#OAUTH2APPNAME,,
        scope,.accessToken,.idtoken,.responseProperties,.error) {
          set %response.ServerSideRedirect="Web.OAUTH2.Cache2N.cls"
     }
     quit 1
    }

    上記のコードにある SYS.OAuth2.AccessToken クラスの IsAuthorized メソッドは有効なアクセストークンが存在するかどうかをチェックし、存在しない場合に認可サーバーの認証フォームを指すログインボタン/リンクを使用してページの内容を表示し、存在する場合に実際にデータ取得処理を実行する 2 番目のページにリダイレクトします。

    ただし、このコードは次のように変更できます。

    ClassMethod OnPreHTTP() As %Boolean [ ServerOnly = 1 ]
    {
     set scope="openid profile scope1 scope2"
     set sc=##class(%SYS.OAuth2.Authorization).GetAccessTokenAuthorizationCode(
        ..#OAUTH2APPNAME,scope,..#OAUTH2CLIENTREDIRECTURI,.properties)
     quit +sc
    }

    この場合は、結果が異なります。 %SYS.OAuth2.Authorization クラスの GetAccessTokenAuthorizationCode メソッドを使用すると、アプリケーションの最初のページの内容を表示せずに、認可サーバーの認証フォームに直接移動します。

    これは、Web アプリケーションがモバイルデバイスのネイティブアプリケーションから呼び出され、一部のユーザー情報がネイティブアプリケーション(ランチャー)によってすでに表示されており、認可サーバーを指すボタンを含む Web ページを表示する必要がない場合に便利です。

    署名付き JWT トークンを使用する場合は、その内容を検証する必要があります。 この検証は次のメソッドで実行されます。

     set valid=##class(%SYS.OAuth2.Validation).ValidateJWT(applicationName,accessToken,scope,,.jsonObject,.securityParameters,.sc)
    

    メソッドパラメーターの詳細については、Class Reference ドキュメントをご覧ください。

    カスタマイズ

    OAUTH が認証 / 承認 UI のカスタマイズ用に提供しているオプションについてもう少し説明します。

    勤務先のポリシーで、スコープの付与に関してより限定的な動作が要求されているとします。 たとえば、取引先銀行内のさまざまな金融システムに接続するホームバンキングアプリケーションを実行できるとしましょう。 銀行は、取得対象の実際の銀行口座に関する情報を含むスコープへのアクセスのみを許可します。 銀行は非常に多くの口座を運営しているため、すべての口座に静的なスコープを定義することは不可能です。 代わりに、認可処理中にその場でスコープを生成する処理をカスタム認可ページのコードに組み込むことができます。

    デモを行うため、ここではサーバー構成にもう 1 つのスコープを追加する必要があります。次の画像を参照してください。

    また、%OAuth2.Server.Authenticate.Bank という名前のカスタム Authenticate クラスへの参照も追加しました。

    では、銀行の認証クラスはどのようになるのでしょうか? 次は想定されるクラスの例です。 このクラスは、ユーザーが提供するデータを使用して標準の認証フォームと認可フォームを拡張します。 BeforeAuthenticateDisplayPermissionsAfterAuthenticate の各メソッド間を流れる情報は、%OAuth2.Server.Properties クラスの properties 変数によって渡されます。

    Class %OAuth2.Server.Authenticate.Bank Extends %OAuth2.Server.Authenticate
    {
    /// account(口座)のスコープに CUSTOM BESTBANK のサポートを追加します。
    ClassMethod BeforeAuthenticate(scope As %ArrayOfDataTypes, properties As %OAuth2.Server.Properties) As %Status
    {
     // 起動スコープが指定されていない場合は何もしません。
     If 'scope.IsDefined("account") Quit $$$OK
     // 起動クエリパラメーターから起動コンテキストを取得します。
     Set tContext=properties.RequestProperties.GetAt("accno")
     // コンテキストがない場合は何もしません。
     If tContext="" Quit $$$OK
        
     try {
        // ここで BestBank コンテキストを照会する必要があります。
        Set tBankAccountNumber=tContext
        // accno のスコープを追加します。 -> 動的にスコープを変更(account なし:<accno> スコープはサーバー構成に存在します)
        // この特定のスコープは、それが Cookie サポートを使用して account または account:accno によって
        // 以前に選択されていた場合に account 経由で同じ accno にアクセスできるようにするために使用されます。
        Do scope.SetAt("Access data for account "_tBankAccountNumber,"account:"_tBankAccountNumber)
        // 処理が終わった account のスコープはもう必要ありません。
        // これにより、account スコープが存在することで DisplayPermissions が強制的に呼び出されるのを防ぎます。
        Do scope.RemoveAt("account")
        
        // AfterAuthenticate が応答プロパティに変換する accno プロパティを追加します。
        Do properties.CustomProperties.SetAt(tBankAccountNumber,"account_number")
     } catch (e) {
        s ^dk("err",$i(^dk("err")))=e.DisplayString()
     }
     Quit $$$OK
    }
    
    /// account のスコープに CUSTOM BESTBANK のサポートを追加します。
    /// account_number カスタムプロパティが BeforeAuthenticate(account)または
    /// DisplayPermissions(account:accno)によって追加された場合は、必要な応答プロパティを追加します。
    ClassMethod AfterAuthenticate(scope As %ArrayOfDataTypes, properties As %OAuth2.Server.Properties) As %Status
    {
     // account_number(account)または accno(account:accno)プロパティが存在しない限り、ここで実行することは何もありません。
     try {
        // カスタムログを記録する例
        If $$$SysLogLevel>=3 {
         Do ##class(%OAuth2.Utils).LogServerScope("log ScopeArray-CUSTOM BESTBANK",%token)
        }
        If properties.CustomProperties.GetAt("account_number")'="" {
         // 応答に accno クエリパラメーターを追加します。
         Do properties.ResponseProperties.SetAt(properties.CustomProperties.GetAt("account_number"),"accno")
        }
     } catch (e) {
        s ^dk("err",$i(^dk("err")))=e.DisplayString()
     }
     Quit $$$OK
    }
    
    /// BEST BANK の account のテキストを含むように変更された DisplayPermissions
    ClassMethod DisplayPermissions(authorizationCode As %String, scopeArray As %ArrayOfDataTypes, currentScopeArray As %ArrayOfDataTypes, properties As %OAuth2.Server.Properties) As %Status
    {
     Set uilocales = properties.RequestProperties.GetAt("ui_locales")
     Set tLang = ##class(%OAuth2.Utils).SelectLanguage(uilocales,"%OAuth2Login")
     // $$$TextHTML(Text,Domain,Language)
     Set ACCEPTHEADTITLE = $$$TextHTML("OAuth2 Permissions Page","%OAuth2Login",tLang)
     Set USER = $$$TextHTML("User:","%OAuth2Login",tLang)
     Set POLICY = $$$TextHTML("Policy","%OAuth2Login",tLang)
     Set TERM = $$$TextHTML("Terms of service","%OAuth2Login",tLang)
     Set ACCEPTCAPTION = $$$TextHTML("Accept","%OAuth2Login",tLang)
     Set CANCELCAPTION = $$$TextHTML("Cancel","%OAuth2Login",tLang)
     &html<<html>>
     Do ..DrawAcceptHead(ACCEPTHEADTITLE)
     Set divClass = "permissionForm"
     Set logo = properties.ServerProperties.GetAt("logo_uri")
     Set clientName = properties.ServerProperties.GetAt("client_name")
     Set clienturi = properties.ServerProperties.GetAt("client_uri")
     Set policyuri = properties.ServerProperties.GetAt("policy_uri")
     Set tosuri = properties.ServerProperties.GetAt("tos_uri")
     Set user = properties.GetClaimValue("preferred_username")
     If user="" {
        Set user = properties.GetClaimValue("sub")
     }
     &html<<body>>
     &html<<div id="topLabel"></div>>
     &html<<div class="#(divClass)#">>
     If user '= "" {
        &html<
         <div>
         <span id="left" class="userBox">#(USER)#<br>#(##class(%CSP.Page).EscapeHTML(user))#</span>
         >
     }
     If logo '= "" {
        Set espClientName = ##class(%CSP.Page).EscapeHTML(clientName)
       &html<<span class="logoClass"><img src="#(logo)#" alt="#(espClientName)#" title="#(espClientName)#" align="middle"></span>>
     }
     If policyuri '= "" ! (tosuri '= "") {
       &html<<span id="right" class="linkBox">>
        If policyuri '= "" {
         &html<<a href="#(policyuri)#" target="_blank">#(POLICY)#</a><br>>
        }
        If tosuri '= "" {
         &html<<a href="#(tosuri)#" target="_blank">#(TERM)#</a>>
        }
       &html<</span>>
     }
     &html<</div>>
     &html<<form>>
     Write ##class(%CSP.Page).InsertHiddenField("","AuthorizationCode",authorizationCode),!
     &html<<div>>
     If $isobject(scopeArray), scopeArray.Count() > 0 {
        Set tTitle = $$$TextHTML(" is requesting these permissions:","%OAuth2Login",tLang)
       &html<<div class="permissionTitleRequest">>
        If clienturi '= "" {
         &html<<a href="#(clienturi)#" target="_blank">#(##class(%CSP.Page).EscapeHTML(clientName))#</a>>
        } Else {
         &html<#(##class(%CSP.Page).EscapeHTML(clientName))#>
        }
       &html<#(##class(%CSP.Page).EscapeHTML(tTitle))#</div>>
        Set tCount = 0
        Set scope = ""
        For {
         Set display = scopeArray.GetNext(.scope)
         If scope = "" Quit
         Set tCount = tCount + 1
         If display = "" Set display = scope
         Write "<div class='permissionItemRequest'>"_tCount_". "_##class(%CSP.Page).EscapeHTML(display)_"</div>"
        }
     }
    
     If $isobject(currentScopeArray), currentScopeArray.Count() > 0 {
        Set tTitle = $$$TextHTML(" already has these permissions:","%OAuth2Login",tLang)
       &html<<div>>
       &html<<div class="permissionTitleExisting">>
        If clienturi '= "" {
         &html<<a href="#(clienturi)#" target="_blank">#(##class(%CSP.Page).EscapeHTML(clientName))#</a>>
        } Else {
         &html<#(##class(%CSP.Page).EscapeHTML(clientName))#>
        }
       &html<#(##class(%CSP.Page).EscapeHTML(tTitle))#</div>>
        Set tCount = 0
        Set scope = ""
        For {
         Set display = currentScopeArray.GetNext(.scope)
         If scope = "" Quit
         Set tCount = tCount + 1
         If display = "" Set display = scope
         Write "<div class='permissionItemExisting'>"_tCount_". "_##class(%CSP.Page).EscapeHTML(display)_"</div>"
        }
       &html<</div>>
     }
    
     /*********************************/
     /*  BEST BANK CUSTOMIZATION      */
     /*********************************/
     try {
        If properties.CustomProperties.GetAt("account_number")'="" {
         // Display the account number obtained from account context.
         Write "<div class='permissionItemRequest'><b>Selected account is "_properties.CustomProperties.GetAt("account_number")_"</b></div>",!
    
         // or, alternatively, let user add some more information at this stage (e.g. linked account number)
         //Write "<div>Account Number: <input type='text' id='accno' name='p_accno' placeholder='accno' autocomplete='off' ></div>",!
        }
     } catch (e) {
        s ^dk("err",$i(^dk("err")))=e.DisplayString()
     }
    
     /* original implementation code continues here... */
     &html<
       <div><input type="submit" id="btnAccept" name="Accept" value="#(ACCEPTCAPTION)#"/></div>
       <div><input type="submit" id="btnCancel" name="Cancel" value="#(CANCELCAPTION)#"/></div>
        >
     &html<</form>
     </div>>
     Do ..DrawFooter()
     &html<</body>>
     &html<<html>>
     Quit 1
    }
    
    /// CUSTOM BESTBANK の場合、入力された患者を検証する必要があります。
    /// ! このメソッドの javascript はユーザーに追加データを DisplayPermissions メソッド内
    /// で入力させる場合にのみ必要です !
    ClassMethod DrawAcceptHead(ACCEPTHEADTITLE)
    {
     &html<<head><title>#(ACCEPTHEADTITLE)#</title>>
     Do ..DrawStyle()
     &html<
     <script type="text/javascript">
     function doAccept()
     {
        var accno = document.getElementById("accno").value;
        var errors = "";
        if (accno !== null) {
         if (accno.length < 1) {
           errors = "Please enter account number name";
         }
        }
        if (errors) {
         alert(errors);
         return false;
        }
        
        // submit the form
        return true;
     }
     </script>
     >
     &html<</head>>
    }
    
    }
    
    

    ご覧のとおり、%OAuth2.Server.Properties クラスにはいくつかの配列が含まれており、渡されています。 具体的には、以下の配列です。

    ·        RequestProperties – 認可リクエストのパラメーターが含まれています。

    ·        CustomProperties – 上記のコードの間でデータをやり取りするためのコンテナ。

    ·        ResponseProperties – トークンリクエストに対する JSON 応答オブジェクトに追加されるプロパティのコンテナ。

    ·        ServerProperties – 認可サーバーがカスタマイズコードに公開する共有プロパティが含まれます(logo_uri、client_uri など…)

    さらに、認可サーバーが返す必要のあるクレームを指定するのに使用されるいくつかの "claims" プロパティが含まれています。

    この認証ページを正しく呼び出すため、最初のクライアントページのコードを次のように変更しました。

     set scope="openid profile scope1 scope2 account"
     // このデータはアプリケーションに由来し(フォームデータなど)、リクエストのコンテキストを設定します。
     // ここでは Authenticate クラスをサブクラス化することで、このデータをユーザーに表示することができます。
     // それにより、該当ユーザーはアクセスを許可するかどうかを決めることができます。
     set properties("accno")="75-452152122-5320"
     set url=##class(%SYS.OAuth2.Authorization).GetAuthorizationCodeEndpoint(
       ..#OAUTH2APPNAME,
        scope,
       ..#OAUTH2CLIENTREDIRECTURI,
        .properties,
       .isAuthorized,
        .sc)
     if $$$ISERR(sc) {
        write "GetAuthorizationCodeEndpoint Error="
       write ..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!
     }

    ご覧のとおり、ここではアプリケーションのさまざまな部分から発生する可能性のあるコンテキスト値を使用して account のスコープとプロパティ配列ノード “accno” を追加しました。 この値はアクセストークンの内部でリソースサーバーに渡され、さらに処理されます。

    上記のロジックは、電子的な患者記録を交換するための FHIR 標準で実際に使用されています。

    デバッグ

    OAUTH フレームワークには、デバッグ機能が組み込まれています。 クライアントとサーバー間の通信はすべて暗号化されているため、これは非常に便利です。 デバッグ機能を使用すると、API クラスによって生成されたトラフィックデータをネットワーク経由で送信する前にキャプチャできます。 コードをデバッグするため、以下のコードに従って単純なルーチンまたはクラスを実装できます。 InterSystems IRIS インスタンスのすべての通信でこのコードを実装する必要があることに注意してください! その場合、OAUTH フローのプロセス内での役割を示す名前をファイル名に指定することをお勧めします。 (以下のサンプルコードは rr.mac ルーチンとして保存されていますが、どんな名前を付けるかはあなた次第です。)

     // d start^rr()
    start() public {
     new $namespace
     set $namespace="%sys"
     kill ^%ISCLOG
     set ^%ISCLOG=5
     set ^%ISCLOG("Category","OAuth2")=5
     set ^%ISCLOG("Category","OAuth2Server")=5
     quit
    }
    
     // d stop^rr()
    stop() public {
     new $namespace
     set $namespace="%sys"
     set ^%ISCLOG=0
     set ^%ISCLOG("Category","OAuth2")=0
     set ^%ISCLOG("Category","OAuth2Server")=0
     quit
    
    }
    
     // display^rr()
    display() public {
     new $namespace
     set $namespace="%sys"
     do ##class(%OAuth2.Utils).DisplayLog("c:\temp\oauth2_auth_server.log")
     quit
    }

    次に、テストを開始する前にターミナルを開き、すべての InterSystems IRIS ノード(クライアント、認可サーバー、リソースサーバー)で d start^rr() を呼び出してください。 完了後、d stop^rr() d display^rr() を実行してログファイルを読み込んでください。

    最後に

    この連載記事では、InterSystems IRIS OAuth 2.0 の実装を使用する方法を学びました。 パート1では簡単なクライアントアプリケーションのデモを行い、パート2では複雑な例を説明しました。 最後に、OAuth 2.0 の実装で最も重要なクラスについて説明し、ユーザーアプリケーション内でそれらを呼び出す必要がある場合について説明しました。

    時々私が投げかけるくだらない質問に我慢強く回答し、この連載をレビューしてくれた Marvin Tener に心から感謝の意を表します。

     

    0
    0 374
    記事 Shintaro Kaminaka · 8月 20, 2020 23m read

    作成者:Daniel Kutac(InterSystems セールスエンジニア) 注意: 使用されている URL に戸惑っている方のために。*元の連載記事では、dk-gs2016 と呼ばれるマシンの画面を使用していました。 新しいスクリーンショットは別のマシンから取得されています。 *WIN-U9J96QBJSAG という URL は dk-gs2016 であると見なしても構いません。

    パート2. 認可サーバー、OpenID Connect サーバー

    この短い連載の前のパートでは、OAUTH[1] クライアントとして機能する単純な使用事例について学びました。 今回は私たちの経験をまったく新しいレベルに引き上げましょう。 InterSystems IRIS がすべての OAUTH の役割を果たす、より複雑な環境を構築します。 クライアントの作成方法はすでに分かっていますので、認可サーバーだけでなく、OpenID Connect[2] プロバイダーにも注意を向けましょう。 前のパートと同様に、環境を準備する必要があります。 今回はより多くの変動要素があるため、より注意を要します。

    具体例を見る前に、OpenID Connect について少し説明する必要があります。 前のパートの内容を覚えていらっしゃるかと思いますが、Google から認可してもらうため、まずは自身がGoogle で認証を受けることを求められていました。 認証は OAUTH フレームワークには含まれていません。 実際、OAUTH に依存しない多くの認証フレームワークがあります。 そのうちの1つに OpenID と呼ばれるものがあります。 当初は単独の構想で開始されましたが、最近では OAUTH フレームワークによって提供されるインフラストラクチャ、つまり通信とデータ構造が活用されています。 その結果、OpenID Connect が誕生しました。 事実、多くの人がこれを OAUTH の強化版と呼んでいます。 実際、OpenID Connect を使用すると、認可するだけでなく、OAUTH フレームワークのよく知られたインターフェースを使用して認証することもできます。

    複雑な OpenID Connect のデモ

    ここでは、パート 1 のクライアントコードの多くを活用します。 そうすることで多くの手間が省けるため、環境のセットアップに集中できます。

    前提条件

    今回は、SSL が有効になっている既存の Web サーバーに PKI インフラストラクチャを追加する必要があります。 OpenID Connect の要件である暗号化が必要です。 ユーザー認証が必要な場合は、第三者がエージェント(クライアント、認証サーバーなど)になりすますし、ネットワーク経由でユーザーの機密データを送信できないようにする必要があります。 そこで X.509 ベースの暗号化の出番です。

    注意 : Cache 2017.1 以降では、X.509 証明書を使用して JWT / JWKS(JSON Web Key Set)を生成する必要はありません。 ここでは下位互換性と単純化を図るためにこのオプションを使用しています。
    ### PKI 厳密に言えば、Caché PKI インフラストラクチャを使用する必要はまったくありません。ただし、openssl などのツールを直接使用してすべての証明書を生成するよりは楽です。 詳細は InterSystems IRIS の[ドキュメント](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GCAS_pki#GCAS_C157792)や他の場所でも確認できますので、ここでは証明書の生成に関する詳細は触れません。 証明書を生成した結果、3 つの公開鍵/秘密鍵のペアと関連する証明書が作成されます。 これらを次のように呼びます。
    • 発行元認証局の root_ca(root_ca.cer)

    • 認可サーバーおよび OpenID サーバー用の auth(auth.cer および auth.key)

    • クライアントアプリケーションサーバー用の client(client.cer および client.key)

    X.509 資格情報

    デモ中に交換された JSON Web Token(JWT)に署名して検証できるよう、個々のサーバーで X.509 資格情報を定義する必要があります。

    認可サーバーと認証サーバーの構成

    ここでは X.509 資格情報の定義方法については詳しく説明せず、AUTHSERVER インスタンスの資格情報のスクリーンショットを示します。 画像のように、AUTHSERVER にはその秘密鍵と証明書がありますが、CLIENT に関しては公開鍵を含む証明書しかありません。

    クライアントサーバーの構成

    同様に、CLIENT インスタンスで資格情報が定義されています。 ここで、CLIENT には秘密鍵と証明書がありますが、AUTHSERVER に関しては公開鍵を含む証明書しかありません。

    リソースサーバーの構成

    このセットアップの例では、RESSERVER インスタンスで X509 資格情報を定義する必要はありません。

    OAUTH の構成

    この連載のパート 1 で説明した構成と同様に、サーバーを OAUTH 用に構成する必要があります。 まずは、OAUTH 構成全体の中心的なコンポーネントである AUTHSERVER インスタンスから始めましょう。

    AUTHSERVER

    System Management Portal で、System Administration > Security > OAuth 2.0 > Server Configuration を開きます。 メニューのリンクをクリックし、次のフォーム項目に入力します。

    • Host name(ホスト名)

    • Port(ポート、省略可)

    • Prefix(プレフィックス、省略可) – これら 3 つのフィールドは Issuer endpoint(発行者エンドポイント)を構成します。

    • 更新トークンを返す条件を指定します。

    • Supported grant types(サポートされているグラント種別)をチェックします。このデモでは 4 つの種別すべてにチェックします。 ただし、認可コードのみが使用されます。

    • 必要に応じて Audience required(オーディエンスを要求)をチェックします。これにより、aud プロパティが認可コードと暗黙的な許可(Implicit)のリクエストに追加されます。

    • 必要に応じて Support user session(ユーザーセッションをサポート)をチェックします。これにより、認可サーバーが現在このブラウザを使用しているユーザーのログイン状態を維持するために httpOnly Cookie が使用されます。 2 回目以降のアクセストークンの要求では、ユーザー名とパスワードの入力は求められません。

    • Endpoint intervals(エンドポイントの間隔)を指定します。

    • このサーバーでサポートされるスコープ(Supportted scopes)を定義します。

    • Customization Options(カスタマイズオプション)のデフォルト値を受け入れるか、カスタム値を入力します。**注意: **JWT が単なる opaque トークンではなくアクセストークンとして使用されるよう、Generate token class(生成トークンクラス)の値を %OAuth2.Server.Generate から %OAuth2.Server.JWT に変更してください。

    • OAuth 2.0 の要求に従い、HTTP 上の SSL を確立するための登録済み SSL 構成の名前を入力します。

    • JSON Web Token(JWT)の設定を入力します。

    以下はサンプル構成のスクリーンショットです。 サーバー構成を定義したら、サーバークライアント構成を入力する必要があります。 サーバー構成フォームのページ内で、「Client Configurations」(クライアント構成)ボタンをクリックし、CLIENT インスタンスおよび RESSERVER インスタンスの「Create New Configuration」(新しい構成の作成)をクリックします。 (IRISを使用して構成している場合は、「クライアントディスクリプション」の構成ページから以下の設定を行います。) 以下の画像は CLIENT の構成を示しています。 JWT Token(JWT トークン)タブはデフォルト値である空のままにします。 ご覧のとおり、実際のアプリケーションの場合とは異なり、フィールドには無意味なデータが入力されています。 同様に、RESSERVER の構成を示します。 ご覧のとおり、リソースサーバーに必要なのは非常に基本的な情報だけです。具体的には、Client type(クライアント種別)をリソースサーバーに設定する必要があります。 CLIENT では、より詳細な情報とクライアント種別を入力する必要があります(クライアントはクライアントシークレットをサーバーで維持し、クライアントエージェントには送信しない Web アプリケーションとして動作するため、機密(confidential)を選択します)。

    CLIENT

    SMP で、System Administration > Security > OAuth 2.0 > Client Configurations を開きます。 「Create Server Configuration」(サーバー構成の作成)ボタンをクリックし、フォームに入力して保存します。 Issuer Endpoint(発行者エンドポイント)が、AUTHSERVER インスタンスで前に定義した値に対応していることを必ず確認してください! また、認可サーバーのエンドポイントを Web サーバーの構成に従って変更する必要があります。 この場合は各入力フィールドに「authserver」を埋め込んだだけです。 次に、新しく作成された発行者エンドポイントの横にある「Client Configurations」(クライアント構成)リンクをクリックし、「Create Client Configuration」(クライアント構成の作成)ボタンをクリックします。 以上です! ここまでの手順で CLIENT と AUTHSERVER の両方を構成しました。 多くの使用事例ではこれだけで十分です。リソースサーバーは単なる AUTHSERVER のネームスペースにすぎず、結果的にすでに保護されている場合があるからです。 しかし、外部の医師が内部の臨床システムからデータを取得しようとしている使用事例に対応する場合を考えてみましょう。 このような医師がデータを取得できるようにするため、この医師のアカウント情報を監査目的と法医学的な理由でリソースサーバー内に確実に保存したいと思います。 この場合は、続けて RESSERVER を定義する必要があります。

    RESSERVER

    SMP で、System Administration > Security > OAuth 2.0 > Client Configurations を開きます。 「Create Server Configuration」(サーバー構成の作成)ボタンをクリックし、フォームに入力して保存します。 ここでは、Cache 2017.1 に実装された新機能である検出機能を使用しました。 ご覧のように、この構成は CLIENT インスタンスの対応する構成と同じデータを使用しています。 次に、新しく作成された発行者エンドポイントの横にある「Client Configurations」(クライアント構成)リンクをクリックし、「Create Client Configuration」(クライアント構成の作成)ボタンをクリックします。 X.509 資格情報から JWT を作成することはお勧めしませんが、ここでは互換性のために使用しました。 CLIENTとほとんど同じ手順でしたが、必要なものでした。 しかし、これで先に進んでコーディングできるようになりました!

    クライアントアプリケーション

    話をできる限り簡単にするため、パート1で説明した Google の例から多くのコードをリサイクルします。 クライアントアプリケーションは /csp/myclient で実行されるわずか 2 つの CSP ページからなるアプリケーションです。セキュリティは強制せず、未認証ユーザーとして実行されます。

    ページ 1

    Class Web.OAUTH2.Cache1N Extends %CSP.Page
    {
    
    Parameter OAUTH2CLIENTREDIRECTURI = "https://dk-gs2016/client/csp/myclient/Web.OAUTH2.Cache2N.cls";
    
    Parameter OAUTH2APPNAME = "demo client";
    
    ClassMethod OnPage() As %Status
    {
      &html<<html>
    
    <body>
      <h1>Cache&acute; OAuth2 プロバイダーに対する認証と認可</h1>
      <p>このページのデモでは、OAuth2 の認可を使用して Cache&acute; API 関数を呼び出す方法を示しています。
      <p>ここでは Cache&acute; の認証および認可サーバーを呼び出し、アプリケーションに別の Cache&acute; サーバーに保存されているデータへのアクセスを許可します。
     >
    
      // 適切なリダイレクトとスコープを持つ認証エンドポイントの URL を取得します。
      // 返された URL は下のボタンで使用されます。
    
      // DK: 'dankut' アカウントを使用して認証します!
      set scope="openid profile scope1 scope2"
      set url=##class(%SYS.OAuth2.Authorization).GetAuthorizationCodeEndpoint(
        ..#OAUTH2APPNAME,
        scope,
        ..#OAUTH2CLIENTREDIRECTURI,
        .properties,
        .isAuthorized,
        .sc)
      if $$$ISERR(sc) {
        write "GetAuthorizationCodeEndpoint Error="
        write ..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!
      } 
    
      &html<
      <div class="portalLogoBox"><a class="portalLogo" href="#(url)#">Authorize for <b>ISC</b></a></div>
      </body></html>>
      Quit $$$OK
    }
    
    ClassMethod OnPreHTTP() As %Boolean [ ServerOnly = 1 ]
    {
      #dim %response as %CSP.Response
      set scope="openid profile scope1 scope2"
      if ##class(%SYS.OAuth2.AccessToken).IsAuthorized(..#OAUTH2APPNAME,,scope,.accessToken,.idtoken,.responseProperties,.error) {
        set %response.ServerSideRedirect="Web.OAUTH2.Cache2N.cls"
      }
      quit 1
    }
    
    }
    

    ページ 2

    Class Web.OAUTH2.Cache2N Extends %CSP.Page
    {
    
    Parameter OAUTH2APPNAME = "demo client";
    
    Parameter OAUTH2ROOT = "https://dk-gs2016/resserver";
    
    Parameter SSLCONFIG = "SSL4CLIENT";
    
    ClassMethod OnPage() As %Status
    {
        &html<<html>
      
    <body>>
        
        // OAuth2 サーバーからのアクセストークンがあるかどうかをチェックします。
        set isAuthorized=##class(%SYS.OAuth2.AccessToken).IsAuthorized(..#OAUTH2APPNAME,,"scope1 scope2",.accessToken,.idtoken,.responseProperties,.error)
        
        // アクセストークンがあるかどうかをさらにチェックします。
        // 以下は実行可能なすべてのテストであり、あらゆるケースで必要なわけではありません。
        // 各テストで返される JSON オブジェクトが単に表示されています。
        if isAuthorized {
            write "<h3>Authorized!</h3>",!
            
            
            // JWT の場合、検証してからアクセストークンから詳細を取得します。
            set valid=##class(%SYS.OAuth2.Validation).ValidateJWT(..#OAUTH2APPNAME,accessToken,"scope1 scope2",,.jsonObject,.securityParameters,.sc)
            if $$$ISOK(sc) {
                if valid {
                    write "Valid JWT"_"<br>",!    
                } else {
                    write "Invalid JWT"_"<br>",!    
                }
                write "Access token="
                do jsonObject.%ToJSON()
                write "<br>",!
            } else {
                write "JWT Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
            }
            write "<br>",!
    
            // イントロスペクションエンドポイントを呼び出して結果を表示します。RFC 7662 を参照してください。
            set sc=##class(%SYS.OAuth2.AccessToken).GetIntrospection(..#OAUTH2APPNAME,accessToken,.jsonObject)
            if $$$ISOK(sc) {
                write "Introspection="
                do jsonObject.%ToJSON()
                write "<br>",!
            } else {
                write "Introspection Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
            }
            write "<br>",!
            
            if idtoken'="" {
                // ID トークンの検証と表示。OpenID Connect Core の仕様を参照してください。
                set valid=##class(%SYS.OAuth2.Validation).ValidateIDToken(
                    ..#OAUTH2APPNAME,
                    idtoken,
                    accessToken,,,
                    .jsonObject,
                    .securityParameters,
                    .sc)
                if $$$ISOK(sc) {
                    if valid {
                        write "Valid IDToken"_"<br>",!    
                    } else {
                        write "Invalid IDToken"_"<br>",!    
                    }
                    write "IDToken="
                    do jsonObject.%ToJSON()
                    write "<br>",!
                } else {
                    write "IDToken Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
                }
            } else {
                write "No IDToken returned"_"<br>",!
            }
            write "<br>",!
        
            // アプリケーションロジックには不要ですが、委任認証に渡すことができるユーザーに関する情報を提供します。
        
            // Userinfo エンドポイントを呼び出して結果を表示します。OpenID Connect Core の仕様を参照してください。
            set sc=##class(%SYS.OAuth2.AccessToken).GetUserinfo(
                ..#OAUTH2APPNAME,
                accessToken,,
                .jsonObject)
            if $$$ISOK(sc) {
                write "Userinfo="
                do jsonObject.%ToJSON()
                write "<br>",!
            } else {
                write "Userinfo Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
            }
            write "<p>",!
    
            /***************************************************
            *                                                  *
            *   リソースサーバーを呼び出し、結果を表示します。   *
            *                                                  *
            ***************************************************/
                    
            // オプション 1 - リソースサーバー - 定義によれば認可サーバーからのデータを信頼します。
            //     そのため、リソースサーバーに渡されたアクセストークンが有効である限り
            //  要求元を問わずデータを提供します。
            
            // オプション 2 - または委任認証(OpenID Connect)を使用して 
            //  (委任認証を保護して)別の CSP アプリケーションを呼び出すこともできます。
            //  - これはまさにこのデモで実施する内容です。
            
            
            write "<h4>リソースサーバーの呼び出し(委任認証)","</h4>",!
            set httpRequest=##class(%Net.HttpRequest).%New()
            // AddAccessToken は現在のアクセストークンをリクエストに追加します。
            set sc=##class(%SYS.OAuth2.AccessToken).AddAccessToken(
                httpRequest,,
                ..#SSLCONFIG,
                ..#OAUTH2APPNAME)
            if $$$ISOK(sc) {
                set sc=httpRequest.Get(..#OAUTH2ROOT_"/csp/portfolio/oauth2test.demoResource.cls")
            }
            if $$$ISOK(sc) {
                set body=httpRequest.HttpResponse.Data
                if $isobject(body) {
                    do body.Rewind()
                    set body=body.Read()
                }
                write body,"<br>",!
            }
            if $$$ISERR(sc) {
                write "Resource Server Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
            }
            write "<br>",!
        
            write "<h4>Call resource server - no auth, just token validity check","</h4>",!
            set httpRequest=##class(%Net.HttpRequest).%New()
            // AddAccessTokenは現在のアクセストークンをリクエストに追加します。
            set sc=##class(%SYS.OAuth2.AccessToken).AddAccessToken(
                httpRequest,,
                ..#SSLCONFIG,
                ..#OAUTH2APPNAME)
            if $$$ISOK(sc) {
                set sc=httpRequest.Get(..#OAUTH2ROOT_"/csp/portfolio2/oauth2test.demoResource.cls")
            }
            if $$$ISOK(sc) {
                set body=httpRequest.HttpResponse.Data
                if $isobject(body) {
                    do body.Rewind()
                    set body=body.Read()
                }
                write body,"<br>",!
            }
            if $$$ISERR(sc) {
                write "Resource Server Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
            }
            write "<br>",!
        } else {
            write "Not Authorized!<p>",!
            write "<a href='Web.OAUTH2.Cache1N.cls'>Authorize me</a>"
        }    
        &html<</body></html>>
        Quit $$$OK
    }
    
    }
    

    次のスクリーンショットで処理を説明します。

    AUTHSERVER インスタンスの認可 / OpenID Connect 認証サーバーのログインページ

    AUTHSERVER のユーザー同意ページ

    その後、最終的に結果ページが表示されます。

    ご覧のとおり、実際にコードを読んでもパート 1 で示したクライアントのコードとほとんど違いはありません。 ページ 2 には違いがあります。 これはデバッグ情報であり、JWT の有効性をチェックしています。 返ってきた JWT を検証した後、AUTHSERVER からのユーザー識別情報に関するデータに対してイントロスペクションを実行できました。 ここではこの情報をページに出力しただけですが、それ以上のこともできます。 上記で説明した外部の医師の使用事例と同様に、必要に応じて識別情報を使用し、それを認証目的でリソースサーバーに渡すことができます。 または、この情報をパラメーターとしてリソースサーバーへの API 呼び出しに渡すこともできます。

    次の段落では、ユーザー識別情報の使用方法について詳しく説明します。

    リソースアプリケーション

    リソースサーバーは認可 / 認証サーバーと同じサーバーにすることができ、多くの場合はそうなります。 しかし、このデモでは 2 つのサーバーを独立した InterSystems IRIS インスタンスにしました。

    したがって、リソースサーバーでセキュリティコンテキストを使用する方法には 2 つあります。

    方法 1 – 認証なし

    これは最も単純なケースです。 認可 / 認証サーバーはまったく同じ Caché インスタンスです。 この場合、単一の目的のために特別に作成された csp アプリケーションにアクセストークンを渡すだけで、OAUTH を使用するクライアントアプリケーションにデータを提供し、データを要求することを認可できます。

    リソース csp アプリケーションの構成(ここでは /csp/portfolio2 と呼びました)は、以下のスクリーンショットのようになります。

    最小限のセキュリティをアプリケーション定義に組み込み、特定の CSP ページのみを実行できるようにします。

    または、リソースサーバーは従来の Web ページの代わりに REST API を提供できます。 実際のシナリオでは、セキュリティコンテキストを微調整するのはユーザー次第です。

    ソースコードの例:

    Class oauth2test.demoResource Extends %CSP.Page
    {
    
    ClassMethod OnPage() As %Status
    {
        set accessToken=##class(%SYS.OAuth2.AccessToken).GetAccessTokenFromRequest(.sc)
        if $$$ISOK(sc) {
            set sc=##class(%SYS.OAuth2.AccessToken).GetIntrospection("RESSERVER resource",accessToken,.jsonObject)
            if $$$ISOK(sc) {        
                // 必要に応じて jsonObject のフィールドを検証します
    
                w "<p><h3>Hello from Cach&eacute; server: <i>/csp/portfolio2</i> application!</h3>"
                w "<p>running code as <b>$username = "_$username_"</b> with following <b>$roles = "_$roles_"</b> at node <b>"_$p($zu(86),"*",2)_"</b>."
            }
        } else {
            w "<h3>NOT AUTHORIZED!</h3>"    
            w "<pre>"
            w
            i $d(%objlasterror) d $system.OBJ.DisplayError()
            w "</pre>"
        }
        Quit $$$OK
    }
    
    }
    

    方法 2 – 委任認証

    これはもう 1 つの極端なケースです。ユーザーがリソースサーバーの内部ユーザーと同等のセキュリティコンテキストで作業しているかのように、リソースサーバーでユーザーの識別情報を可能な限り最大限に活用したいと考えています。

    解決方法の 1 つは、委任認証を使用することです。

    この方法を実行するには、さらにいくつかの手順を実行してリソースサーバーを構成する必要があります。

    ·        委任認証を有効にする

    ·        ZAUTHENTICATE ルーチンを提供する

    ·        Web アプリケーションを構成する(この場合、/csp/portfolio で呼び出しました)

    ここではユーザー識別情報とそのスコープ(セキュリティプロファイル)を提供した AUTHSERVER を信頼しているため、ZAUTHENTICATE ルーチンの実装は非常に単純で簡単です。そのため、ここではいかなるユーザー名も受け入れ、それをスコープと共にリソースサーバーのユーザーデータベースに渡しています(OAUTH スコープと InterSystems IRIS のロール間で必要な変換を行ったうえで)。 それだけです。 残りの処理は InterSystems IRIS によってシームレスに行われます。

    これは ZAUTHENTICATE ルーチンの例です。

    #include %occErrors
    #include %occInclude
    
    ZAUTHENTICATE(ServiceName, Namespace, Username, Password, Credentials, Properties) PUBLIC
    {
        set tRes=$SYSTEM.Status.OK()
        try {        
            set Properties("FullName")="OAuth account "_Username
            //set Properties("Roles")=Credentials("scope")
            set Properties("Username")=Username
            //set Properties("Password")=Password
            // Credentials 配列を GetCredentials() メソッドから渡せないため、一時的に書き換えます。
            set Properties("Password")="xxx"    // OAuth2 アカウントのパスワードは気にしません。
            set Properties("Roles")=Password
        } catch (ex) {
            set tRes=$SYSTEM.Status.Error($$$AccessDenied)
        }
        quit tRes
    }
    
    GetCredentials(ServiceName,Namespace,Username,Password,Credentials) Public 
    {
        s ts=$zts
        set tRes=$SYSTEM.Status.Error($$$AccessDenied)        
    
         try {
             If ServiceName="%Service_CSP" {
                set accessToken=##class(%SYS.OAuth2.AccessToken).GetAccessTokenFromRequest(.sc)
                if $$$ISOK(sc) {
                    set sc=##class(%SYS.OAuth2.AccessToken).GetIntrospection("RESSERVER resource",accessToken,.jsonObject)
                    if $$$ISOK(sc) {
                        // ToDo: 標準アカウントと委任されたアカウント(OpenID)が競合する可能性があるため、注意してください!
                        set Username=jsonObject.username
                        set Credentials("scope")=$p(jsonObject.scope,"openid profile ",2)
                        set Credentials("namespace")=Namespace
                        // temporary hack
                        //set Password="xxx"
                        set Password=$tr(Credentials("scope")," ",",")
                        set tRes=$SYSTEM.Status.OK()
                    } else {
                        set tRes=$SYSTEM.Status.Error($$$GetCredentialsFailed) 
                    }
                }    
            } else {
                set tRes=$SYSTEM.Status.Error($$$AccessDenied)        
            }
         } catch (ex) {
             set tRes=$SYSTEM.Status.Error($$$GetCredentialsFailed)
        }
        Quit tRes
    }
    

    CSP ページ自体は非常にシンプルになります。

    Class oauth2test.demoResource Extends %CSP.Page
    {
    
    ClassMethod OnPage() As %Status
    {
        // アクセストークン認証は委任認証によって実行されます!
        // もう一度ここで行う必要はありません。
    
        // これはリクエストからアクセストークンを取得し、イントロスペクションエンドポイントを
        // 使用してアクセストークンの有効性を確認するダミーのリソースサーバーです。
        // 通常、応答はセキュリティに関連しませんが、リクエストパラメーターに基づく
        // 興味深いデータが含まれている可能性があります。
        w "<p><h3>Hello from Cach&eacute; server: <i>/csp/portfolio</i> application!</h3>"
        w "<p>running code as <b>$username = "_$username_"</b> with following <b>$roles = "_$roles_"</b> at node <b>"_$p($zu(86),"*",2)_"</b>."
        Quit $$$OK
    }
    
    }
    

    そして最後に、/csp/portfolio の Web アプリケーション構成を示します。

    あなたが本当に心配であったなら、最初のバリエーションで行ったように _Permitted クラス_を設定できたかもしれません。 または、REST API を使用していたかもしれません。 しかし、これらの設定に関してはこの記事の中では説明しません。

    次回は、InterSystems IRIS OAUTH フレームワークによって導入される個々のクラスについて説明します。 それらの API、およびそれらを呼び出すタイミングと場所について説明します。

     

    [1] この記事で OAUTH について言及する場合、常に RFC 6749(https://tools.ietf.org/html/rfc6749)で規定されている OAuth 2.0 を指しています。 ここでは簡略化のために短縮形の OAUTH を使用しています。

    [2] OpenID Connect は OpenID Foundation(http://openid.net/connect)によって管理されています。

    0
    0 446
    記事 Shintaro Kaminaka · 8月 12, 2020 6m read

    最初の記事では、RESTForms(永続クラス用のREST API)について説明をしました。 基本的な機能についてはすでに説明しましたが、ここではクエリ機能を中心とする高度な機能について説明します。

    * 基本クエリ * クエリ引数 * カスタムクエリ ### クエリ

    クエリを使用すると、任意の条件に基づいてデータの一部を取得できます。 RESTFormsには、2種類のクエリがあります。

    * 基本クエリは一度定義すればすべてのRESTFormsクラスに対して機能します。異なっているのはフィールドリストのみです。 * カスタムクエリはそれが指定され、使用できるクラスに対してのみ機能しますが、開発者はクエリの本文に完全にアクセスできます。 ### 基本クエリ

    一度定義すると、すべてのクラスか一部のクラスですぐに使用できます。 基本クエリはシステムによって定義されているものもありますが、開発者が追加することもできます。これらのクエリはすべて、SELECTのフィールドリストのみを定義します。 その他すべて(絞り込み、ページネーションなど)はRESTFormsによって行われます。 form/objects/:class/:query を呼び出すと、単純なクエリを実行できます。 2番目の :query パラメーターはクエリ名(クエリの SELECT と FROM の間の内容)を定義します。 デフォルトのクエリタイプは次のとおりです。

    クエリ説明
    allすべての情報
    infodisplayName と id
    infoclassdisplayName、id、class
    count行数

    例えば、Form.Test.Personオブジェクトに関する基本的な情報を取得するには、infoclassクエリを実行できます。 form/objects/Form.Test.Person/infoclass

     {"children": [
        {"_id":"1", "displayName":"Alice",   "_class":"Form.Test.Person"},
        {"_id":"2", "displayName":"Charlie", "_class":"Form.Test.Person"},
        {"_id":"3", "displayName":"William", "_class":"Form.Test.Person"}
    ]}

    RESTFormsは次の場所で myq という名前のクエリを探します(最初にヒットするまで)。

    1. フォームクラス内のqueryMYQクラスメソッド
    2. クエリクラス内のMYQパラメーター
    3. クエリクラス内のqueryMYQクラスメソッド
    4. Form.REST.Objectsクラス内のMYQパラメーター
    5. Form.REST.Objectsクラス内のqueryMYQクラスメソッド

    独自のクエリクラスを定義できます(上記リストの項目2、3用)。このクエリクラスは、すべてのクラスで使用可能なクエリの定義を保持する特別なクラスです。 そのクラスで myq という名前の独自クエリを定義する手順は以下のとおりです。

    1. (1回だけ)YourClassName クラスを定義します。
    2. そのクラスで MYQ パラメーターか queryMYQ クラスメソッドを定義します。 パラメーターはメソッドよりも優先されます。
    3. メソッドまたはパラメーターは、SQLクエリのSELECTとFROMの間の部分を返す必要があります。
    4. (1回だけ)ターミナルで以下を実行します。 Do ##class(For.Settings).setSetting("queryclass", YourClassName)

    メソッドの署名は以下のとおりです。

    ClassMethod queryMYQ(class As %String) As %String

    クラス固有のクエリを定義することもできます。 myq という名前の独自クラスクエリを定義する手順は以下のとおりです。

    1. フォームクラスで queryMYQ クラスメソッドを定義します。
    2. メソッドの署名は以下のとおりです。ClassMethod queryMYQ() As %String
    3. メソッドは、SQLクエリのSELECTとFROMの間の部分を返す必要があります。

    URL 引数

    フィルターやその他のパラメーターをURLで指定できます。 すべての引数は省略可能です。
    引数サンプル値説明
    size2ページサイズ
    page1ページ番号
    filter値+contains+WWHERE句
    orderby値+descORDER BY句
    collationUPPERCOLLATION句
    nocount1レコード数を削除(クエリを高速化します)

    これらの引数に関するいくつかの情報を次に示します。

    ORDER BY句

    結果の順序を変更します。 値は、カラム名またはカラム名+desc です。 カラム名は、SQLテーブルのカラム名またはカラム番号です。

    WHERE句

    絞り込み条件の書式は、カラム名+条件+値です。 カラム名+条件+値+カラム名2+条件2+値2のように、複数の条件を指定できます。 矢印構文とシリアルオブジェクトもサポートされています: Column_ColumnField+条件+値 Valueに空白が含まれている場合、サーバーに送信する前にタブに置き換えます。

    URLSQL
    neq!=
    eq=
    gte>=
    gt>
    lte<=
    lt<
    startswith%STARTSWITH
    contains[
    doesnotcontain'[
    inIN
    likeLIKE

    リクエストの例:

    form/objects/Form.Test.Simple/info?size=2&page=1&orderby=text
    form/objects/Form.Test.Simple/all?orderby=text+desc
    form/objects/Form.Test.Simple/all?filter=text+eq+Hello
    form/objects/Form.Test.Person/infoclass?filter=company_name+contains+a
    form/objects/Form.Test.Simple/all?filter=text+in+A9044~B5920

    SQLアクセスには、ユーザーに適切なSQL権限(フォームテーブルに対するSELECT)を付与する必要があることに注意してください。

    COLLATION句

    書式は、collation=UPPER または collation=EXACT です。 指定した照合順序をWHERE句で強制します。 省略した場合、デフォルトの照合順序が使用されます。

    ページネーション

    ページネーションは、デフォルトで1ページあたり25レコードになります。 ページサイズと現在のページを変更するには、size 引数と page 引数を(1を基準として)指定します。

    カスタムクエリ

    form/objects/:class/custom/:query を呼び出すと、カスタムクエリを実行できます。 カスタムクエリを使用すると、開発者はクエリの内容全体を決めることができます。 size および page 以外のURLパラメーターは指定できません。 メソッドは他のすべてのURLパラメーターを解析する必要があります(またはForm.JSON.SQLからデフォルトのパーサーを呼び出す必要があります)。 myq という名前のカスタムクエリを定義する手順は以下のとおりです。

    1. フォームクラスで customqueryMYQ クラスメソッドを定義します。
    2. メソッドの署名は以下のとおりです。ClassMethod customqueryMYQ() As %String
    3. メソッドは有効なSQLクエリを返す必要があります。

    デモ

    [現在デモ環境はお試しいただくことができません。] こちらでRESTFormsをオンラインで試すことができます(ユーザー名:Demo、パスワード:Demo)。 また、RESTFormsUIアプリケーション(RESTFormsデータエディタ)もあります。こちらをご確認ください(ユーザー名:Demo、パスワード:Demo)。 クラスリストのスクリーンショットを以下に掲載しています。

    まとめ

    RESTFormsは、幅広くカスタマイズ可能なクエリ機能を提供します。

    次の内容

    次の記事では、いくつかの高度な機能について説明します。

    • メタデータの解釈
    • セキュリティと権限
    • オブジェクト名

    リンク

  • RESTForms UI GitHubリポジトリ
  • 0
    0 144
    記事 Shintaro Kaminaka · 8月 7, 2020 5m read

    開発者の皆さん、こんにちは。

    今日はAzure上でIRIS for Healthをデプロイし、FHIRリポジトリを構築する方法をご紹介したいと思います。

    AzureのMarketPlaceで「InterSystems」をキーワードに検索していただくと、以下のように複数のInterSystems製品がヒットします。

    今日はこの製品の中から、InterSystems IRIS for Health Community Editionを選択し、FHIRリポジトリを構築します。

    仮想マシンのサイズや、ディスク、ネットワーク等には特に制約や条件はありません。
    Azureで提供されている最小の構成でもIRIS for Healthを動かすこともできます。

    IRIS for Health Community Editionのデプロイに成功するとこのような画面に遷移します。
    私の例では、コンピュータ名をIRIS4HFHIRSERVERとしています。
    パブリックIPアドレスはマスクしていますが、このIPアドレスを使ってIRIS管理ポータルにアクセスしてみましょう。
    http://<パブリックIPアドレス>:52773/csp/sys/UtilHome.csp

    52773は管理ポータルにアクセスするためのポート番号であり、Azure上でデプロイするとこのポート経由でアクセスできるように既に構成が変更されています。

    0
    0 949
    記事 Shintaro Kaminaka · 8月 3, 2020 6m read

    みなさん、こんにちは。

    Open Exchangeで FHIRリポジトリに接続するFHIR Patient Browserが公開されました。

    https://openexchange.intersystems.com/package/FHIR-Patient-Browser

    Open-Source のJavaScriptライブラリであるfhir.jsを利用した、FHIRサーバに接続するためのFHIRクライアントアプリケーションです。

    この記事では、このOpen Exchangeアプリケーション実行に必要なNode.jsのインストールや、コンパイルおよび実行方法について解説します。(筆者もNode.jsのインストールや実行は初めてです!)

    1.FHIRPatientBrowserのソースのダウンロード

    上記Open Exchangeリンク先の「Download」をクリックすると、以下のGithubサイトへ移動します。

    https://github.com/antonum/FHIRPatientBrowser
     

    以下の画像のようにCodeからZIP形式でダウンロードし、展開します。(もちろんGitなどのツールを利用して入手してもOKです。)

    2. Node.jsのインストール

    Node.jsのインストーラーはこちらのサイトからダウンロード可能です。

    0
    0 387
    記事 Shintaro Kaminaka · 7月 30, 2020 11m read

    この記事では、RESTFormsプロジェクト(モダンなWebアプリケーション用の汎用REST APIバックエンド)を紹介します。

    プロジェクトの背後にあるアイデアは単純です。私はいくつかのREST APIを書いた後、REST APIが一般的に次の2つの部分で構成されていることに気付きました。

    • 永続クラスの操作
    • カスタムビジネスロジック

    また、独自のカスタムビジネスロジックを書く必要はありますが、RESTFormsには永続クラスの操作に関連するすべての機能を提供しています。

    使用例

    • Cachéにすでにデータモデルがあり、REST API形式で情報の一部(またはすべて)を公開したい
    • 新しいCachéアプリケーションを開発しており、REST APIを提供したい

    クライアントサイド

    このプロジェクトはWebアプリケーションのバックエンドとして開発されているため、JSだけで事足ります。 形式の変換は必要ありません。

    補足:CRUD

    オブジェクトまたはコレクションに対し、次の4つの操作を実行できます。

    • Create(作成)
    • Read(読み込み)
    • Update(更新)
    • Delete(削除)

    機能

    RESTFormsを使用して以下を実行できます。

    • 公開されたクラスに対するCRUD - クラスのメタデータを取得し、クラスのプロパティを作成 / 更新 / 削除できます。
    • オブジェクトに対するCRUD - オブジェクトを取得 / 作成 / 更新 / 削除できます。
    • オブジェクトコレクションに対するRead(SQL経由) - SQLインジェクションから保護します。
    • 自己検出 – 最初に使用可能なクラスのリストを取得し、その後でクラスのメタデータを取得し、そのメタデータを基にしてオブジェクトに対するCRUDを実行できます。

    パス

    以下の表には、主なパスとRESTFormsを使用して実行できる操作を掲載しています。

    <td>
      説明
    </td>
    
    <td>
      利用可能なすべてのクラスを一覧表示します
    </td>
    
    <td>
      すべてのクラスのメタデータを取得します
    </td>
    
    <td>
      クラスのメタデータ
    </td>
    
    <td>
      プロパティをクラスに追加します
    </td>
    
    <td>
      クラスのプロパティを変更します
    </td>
    
    <td>
      クラスのプロパティを削除します
    </td>
    
    <td>
      オブジェクトを取得します
    </td>
    
    <td>
      オブジェクトの1つのプロパティを取得します
    </td>
    
    <td>
      オブジェクトを作成します
    </td>
    
    <td>
      動的オブジェクトからオブジェクトを更新します
    </td>
    
    <td>
      オブジェクトからオブジェクトを更新します
    </td>
    
    <td>
      オブジェクトを削除します
    </td>
    
    <td>
      (SQL)クエリでクラスのオブジェクトを取得します
    </td>
    
    <td>
      (SQL)カスタムクエリでクラスのオブジェクトを取得します
    </td>
    
    URL
    info
    info/all
    info/:class
    field/:class
    field/:class
    field/:class/:property
    object/:class/:id
    object/:class/:id/:property
    object/:class
    object/:class/:id
    object/:class
    object/:class/:id
    objects/:class/:query
    objects/:class/custom/:query

    **RESTFormsを使い始めるには?**
    1. GitHubからプロジェクトをインポートします(お勧めの方法は独自リポジトリにサブモジュールとして追加する方法ですが、単にリリースをダウンロードしても良いです)。
    2. RESTFormsを介して公開したい各クラスについて以下を実施します。
    • アダプタクラスから継承する
    • 権限を指定します(一部のクラスを読み取り専用として公開する場合などに実施)。
    • オブジェクトの表示値として使用されるプロパティを指定します。
    • 表示したいプロパティの表示名を指定します。

    セットアップ

    1. [リリースページ](https://github.com/intersystems-ru/RESTForms/releases/tag/v1.0)で最新リリースである20161.xml( Caché 2016.1用)または201162.xml(Caché 2016.2以降用)をダウンロードして任意のネームスペースにインポートします。
    2. 新しいWebアプリケーション /forms をDispatchクラス Form.REST.Main を使用して作成します。
    3. http://localhost:57772/forms/test?Debug をブラウザで開き、インストールを検証します({"Status": "OK"} が出力され、場合によってはパスワードの入力が求められます)。
    4. テストデータが必要な場合は、次を呼び出します:

    do ##class(Form.Util.Init).populateTestForms()

    最初に、利用可能なクラスを知る必要があります。 この情報を取得するには、次を呼び出します。

    http://localhost:57772/forms/form/info
    

    次のような応答が返されます。

    [
       { "name":"Company",     "class":"Form.Test.Company" },
       { "name":"Person",      "class":"Form.Test.Person"  },
       { "name":"Simple form", "class":"Form.Test.Simple"  }
    ]

    現在3つのサンプルクラス(RESTFormで提供)があります。Person(Form.Test.Personクラス)のメタデータを見てみましょう。 この情報を取得するには、次を呼び出します。

    http://localhost:57772/forms/form/info/Form.Test.Person
    

    次のように、クラスのメタデータが応答として返されます。

    {  
       "name":"Person",
       "class":"Form.Test.Person",
       "displayProperty":"name",
       "objpermissions":"CRUD",
       "fields":[  
          { "name":"name",     "type":"%Library.String",    "collection":"", "displayName":"Name",          "required":0, "category":"datatype" },
          { "name":"dob",      "type":"%Library.Date",      "collection":"", "displayName":"Date of Birth", "required":0, "category":"datatype" },
          { "name":"ts",       "type":"%Library.TimeStamp", "collection":"", "displayName":"Timestamp",     "required":0, "category":"datatype" },
          { "name":"num",      "type":"%Library.Numeric",   "collection":"", "displayName":"Number",        "required":0, "category":"datatype" },
          { "name":"аge",      "type":"%Library.Integer",   "collection":"", "displayName":"Age",           "required":0, "category":"datatype" },
          { "name":"relative", "type":"Form.Test.Person",   "collection":"", "displayName":"Relative",      "required":0, "category":"form"     },
          { "name":"Home",     "type":"Form.Test.Address",  "collection":"", "displayName":"House",         "required":0, "category":"serial"   },
          { "name":"company",  "type":"Form.Test.Company",  "collection":"", "displayName":"Company",       "required":0, "category":"form"     }
       ]
    }

    これらの情報は次のような意味を持ちます。

    クラスのメタデータ:

    • name - クラスの表示名。
    • class - 基本となる永続クラス。
    • displayProperty - オブジェクトを表示するときに使用するオブジェクトのプロパティ。
    • objpermissions - ユーザーがオブジェクトを使用して実行できる操作。 この例では、ユーザーは新しいオブジェクトを作成し、既存のオブジェクトを変更し、既存のオブジェクトを削除し、次を取得できます。

    プロパティのメタデータ:

    • name - プロパティ名 - クラスの定義と同じです。
  • type - プロパティのクラス。
  • * コレクション - リスト/配列のコレクションです。 * displayName - 表示プロパティ名。 * required - このプロパティが必須であるかどうか。 * category - プロパティのタイプクラスのカテゴリ。 RESTForms対応のすべてのクラスが「form」として表示されることを除き、通常のCachéクラスのカテゴリに従います。

    クラス定義では次のようになります。

    /// テストフォーム: Person
    Class Form.Test.Person Extends (%Persistent, Form.Adaptor, %Populate)
    {
    
    /// フォーム名。グローバルキーではないため、何でもかまいません。
    /// クラスをフォームとして持たないようにするには(ここのように)空の文字列に設定します。 
    Parameter FORMNAME = "Person";
    
    /// デフォルトの権限
    /// このフォームのオブジェクトは、作成、読み取り、更新、削除できます。
    /// すべてのユーザーの権限を変更するには、このパラメーターを再定義します。
    /// このクラスのcheckPermissionメソッドを再定義します(Form.Securityを参照してください)。
    /// ユーザーやロールなどに基づいて独自のセキュリティを追加します。
    Parameter OBJPERMISSIONS As %String = "CRUD";
    
    /// オブジェクトの基本情報に使用されるプロパティ
    /// デフォルトでは、getObjectDisplayNameメソッドはここから値を取得します。
    Parameter DISPLAYPROPERTY As %String = "name";
    
    /// このパラメーターの値をSQLでORDER BY句の値として使用します。 
    Parameter FORMORDERBY As %String = "dob";
    
    /// Personの名前。
    Property name As %String(COLLATION = "TRUNCATE(250)", DISPLAYNAME = "Name", MAXLEN = 2000);
    
    /// Personの生年月日。
    Property dob As %Date(DISPLAYNAME = "Date of Birth", POPSPEC = "Date()");
    
    Property ts As %TimeStamp(DISPLAYNAME = "Timestamp") [ InitialExpression = {$ZDATETIME($ZTIMESTAMP, 3, 1, 3)} ];
    
    Property num As %Numeric(DISPLAYNAME = "Number") [ InitialExpression = "2.15" ];
    
    /// Personの年齢。<br>
    /// これは、 <property>DOB</property> から派生した値を持つ計算されたフィールドです。
    Property аge As %Integer(DISPLAYNAME = "Age") [ Calculated, SqlComputeCode = { set {*}=##class(Form.Test.Person).currentAge({dob})}, SqlComputed, SqlComputeOnChange = dob ];
    
    /// このクラスメソッドは、誕生日 <var>date</var> が与えられた場合に現在の年齢を計算します。
    ClassMethod currentAge(date As %Date = "") As %Integer [ CodeMode = expression ]
    {
    $Select(date="":"",1:($ZD($H,8)-$ZD(date,8)\10000))
    }
    
    /// Personの配偶者。
    /// これは別の永続オブジェクトへの参照です。
    Property relative As Form.Test.Person(DISPLAYNAME = "Relative");
    
    /// Personの自宅住所。 埋め込みオブジェクトを使用します。
    Property Home As Form.Test.Address(DISPLAYNAME = "House");
    
    /// このPersonが働いている会社。
    Relationship company As Form.Test.Company(DISPLAYNAME = "Company") [ Cardinality = one, Inverse = employees ];
    }

    クラスでRESTFormsを有効にする

    そして、このクラスでRESTFormsを有効にするため、通常の永続クラスから始めて次のことを行いました。

    1. Form.Adaptor から拡張しました。
    2. 値を含むパラメーター FORMNAME(クラス名)を追加しました。
    3. OBJPERMISSIONS パラメーター(すべての権限のCRUD)を追加しました。
    4. DISPLAYPROPERTY パラメーター(オブジェクト名の表示に使用されるプロパティ名)を追加しました。
    5. FORMORDERBY パラメーター(RESTFormsを使用するクエリでソートするデフォルトのプロパティ)を追加しました。
    6. メタデータで確認したいプロパティごとに DISPLAYNAME プロパティのパラメーターを追加しました。

    以上です。 コンパイル後、RESTFormsを含むクラスを使用できるようになります。

    いくつかのテストデータを生成しましたので(インストールのステップ4を参照)、IDが1のPersonを取得してみましょう。 オブジェクトを取得するには、次を呼び出します。

    http://localhost:57772/forms/form/object/Form.Test.Person/1

    その応答は以下のとおりです(生成されるデータは異なる場合があります)。

    {
       "_class":"Form.Test.Person",
       "_id":1,
       "name":"Klingman,Rhonda H.",
       "dob":"1996-10-18",
       "ts":"2016-09-20T10:51:31.375Z",
       "num":2.15,
       "аge":20,
       "relative":null,
       "Home":{
          "_class":"Form.Test.Address",
          "House":430,
          "Street":"5337 Second Place",
          "City":"Jackson"
       },
       "company":{
          "_class":"Form.Test.Company",
          "_id":60,
          "name":"XenaSys.com",
          "employees":[
             null
          ]
       }
    }

    オブジェクト(具体的にはnumプロパティ)を変更するには、次を呼び出します。

    PUT http://localhost:57772/forms/form/object/Form.Test.Person

    このボディを使用します。

    {
       "_class":"Form.Test.Person",
       "_id":1,
       "num":3.15
    }

    速度を上げるには、_class_id、および変更対象のプロパティのみをリクエストのボディに含める必要があります。

    では、新しいオブジェクトを作成しましょう。 以下を呼び出します。

    POST http://localhost:57772/forms/form/object/Form.Test.Person

    このボディを使用します。

    {
       "_class":"Form.Test.Person",
        "name":"Test person",
        "dob":"2000-01-18",
        "ts":"2016-09-20T10:51:31.375Z",
        "num":2.15,
        "company":{ "_class":"Form.Test.Company", "_id":1 }
    }

    オブジェクトの作成が成功した場合、RESTFormsは以下のようにIDを返します。

    {"Id": "101"}

    成功しなかった場合、エラーがJSON形式で返されます。 すべての永続オブジェクトのプロパティは、 _class および _id プロパティによってのみ参照する必要があります。

    そして最後に、新しいオブジェクトを削除しましょう。 以下を呼び出します。

    DELETE http://localhost:57772/forms/form/object/Form.Test.Person/101

    これがForm.Test.Personクラスに対する完全なCRUDです。

    デモ

    [現在デモ環境はお試しいただくことができません。] こちらでRESTFormsをオンラインで試すことができます(ユーザー名:Demo、パスワード:Demo)。

    また、RESTFormsUIアプリケーション(RESTFormsデータエディタ)もあります。こちらをご確認ください(ユーザー名:Demo、パスワード:Demo)。 クラスリストのスクリーンショットを以下に掲載しています。

    まとめ

    RESTFormsは永続クラスに関する、REST APIから要求されるほとんどの機能を提供します。

    次の内容

    この記事では、RESTFormsの機能について説明しました。 次回の記事では、いくつかの高度な機能(クライアントからSQLインジェクションのリスクを冒さずにデータの一部を安全に取得できるクエリなど)についてお話ししたいと思います。 この記事のパート2でクエリに関する情報をお読みください

    RESTFormsUI(RESTFormsデータエディタ)もあります。

    リンク

    0
    0 344
    記事 Shintaro Kaminaka · 7月 3, 2020 17m read

    この記事と後続の2つの連載記事は、InterSystems製品ベースのアプリケーションでOAuth 2.0フレームワーク(簡略化のためにOAUTHとも呼ばれます)を使用する必要のある開発者またはシステム管理者向けのユーザーガイドを対象としています。 

    作成者:Daniel Kutac(InterSystemsシニアセールスエンジニア) 

    公開後の修正および変更の履歴 

    • 2016年8月3日 - 新しいバージョンのページを反映するため、Googleのクライアント設定のスクリーンショットを修正し、Google APIのスクリーンショットを更新しました。
    • 2016年8月28日 - Cache 2016.2でのJSON対応への変更を反映するため、JSON関連コードを変更しました。 
    • 2017年5月3日 - Cache 2017.1でリリースされた新しいUIと機能を反映するため、テキストと画面を更新しました。 
    • 2018年2月19日 - 最新の開発内容を反映するために、CachéをInterSystems IRISに変更しました。 製品名は変更されていますが、この記事はすべてのInterSystems製品(InterSystems IRIS Data Platform、Ensemble、Caché)を対象としています。 

    パート1. クライアント 

    概要 

    これは、3部構成のInterSystemsによるOpen Authorization Frameworkの実装に関する連載記事の最初の記事です。 

    この最初のパートでは、このトピックについて簡単に紹介し、InterSystems IRISアプリケーションが認証サーバーのクライアントとして機能し、保護されたリソースを要求する簡単なシナリオを示します。 

    パート2ではより複雑なシナリオについて説明します。そこではInterSystems IRIS自体が認証サーバーとして機能するほか、OpenID Connectを介した認証サーバーとしても機能します。 

    このシリーズの最後のパートでは、OAUTHフレームワーククラスの個々の部分について説明します。それらはInterSystems IRISにより実装されているからです。 

    Open Authorization Framework[1]とは 

    皆さんの多くはすでにOpen Authorization Frameworkとその使用目的について聞いたことがあるかと思います。 そのため、この記事では初めて同フレームワークを耳にした方のために簡単な要約を掲載します。 

    現在はバージョン2.0であるOpen Authorization Framework(OAUTH)は、クライアント(データを要求するアプリケーション)とリソース所有者(要求されたデータを保持するアプリケーション)の間に間接的な信頼を確立することにより、主にWebベースのアプリケーションが安全な方法で情報を交換できるようにするプロトコルです。 この信頼自体は、クライアントとリソースサーバーの両方が認識して信頼する機関によって提供されます。 この機関は認証サーバーと呼ばれます。 

    次の事例を使用して簡単に説明します。 

    Jenny(OAUTH用語ではリソース所有者)がJennyCorp社のプロジェクトに取り組んでいるとします。 彼女はより大きな潜在的なビジネスのプロジェクト計画を作成し、JohnInc社のビジネスパートナーであるJohn(クライアントユーザー)にドキュメントのレビューを依頼します。 ただし、彼女はジョンに自社のVPNへのアクセスを許可することを快く思っていないので、ドキュメントをGoogleドライブ(リソースサーバー)または他の同様のクラウドストレージに置いています。 そうすることで、彼女は彼女とGoogle(認証サーバー)の間に信頼関係を確立していました。 彼女はJohnと共有するドキュメントを選びます(JohnはすでにGoogleドライブを使用しており、Jennyは彼のメールアドレスを知っています)。 

    Johnはドキュメントを閲覧したいときには自分のGoogleアカウントで認証し、モバイルデバイス(タブレットやノートパソコンなど)からドキュメントエディタ(クライアントサーバー)を起動し、Jennyのプロジェクトファイルを読み込みます。 

    とてもシンプルに聞こえますが、2人とGoogleの間には多くの通信が発生しています。 どの通信もOAuth 2.0仕様に準拠しているため、Johnのクライアント(リーダーアプリケーション)は最初にGoogleで認証する必要があります(OAUTHはこのステップに対応していません)。ジョンがGoogleが提供するフォームにJohnが同意して認証すると、Googleはアクセストークンを発行し、リーダーアプリケーションにドキュメントへのアクセスを許可します。 リーダーアプリケーションはアクセストークンを使用してGoogleドライブにリクエストを発行し、Jennyのファイルを取得します。 

    以下の図に、個々の当事者間の通信を示しています。 

    注意: どのOAUTH 2.0通信もHTTPリクエストを使用していますが、サーバーは必ずしもWebアプリケーションである必要はありません。 

    InterSystems IRISを使ってこの簡単なシナリオを説明しましょう。 

    簡単なGoogleドライブのデモ 

    このデモでは、私たち自身のアカウントを使ってGoogleドライブに保存されているリソース(ファイルのリスト)をリクエストする小さなCSPベースのアプリケーションを作成します(ついでにカレンダーのリストも取得します)。 

    前提条件 

    アプリケーションのコーディングを始める前に、環境を準備する必要があります。 この環境には、SSLが有効になっているWebサーバーとGoogleのプロファイルが含まれます。 

    Webサーバーの構成 

    上記のように、認証サーバーとはSSLを使用して通信する必要があります。これは、OAuth 2.0がデフォルトでSSLを要求するためです。 データを安全に保つためには必要なことですよね? 

    この記事ではSSLをサポートするようにWebサーバーを構成する方法は説明しませんので、お好みの各Webサーバーのユーザーマニュアルを参照してください。 皆さんの好奇心をそそるため、この詳細な例ではMicrosoft IISサーバーを使用します(後でいくつかのスクリーンショットを掲載するかもしれません)。 

    Googleの構成 

    Googleに登録するには、Google API Manager(https://console.developers.google.com/apis/library?project=globalsummit2016demo)を使用する必要があります 

    デモのために、GlobalSummit2016Demoというアカウントを作成しました。 Drive APIが有効になっていることを確認してください。 

    次に、認証情報を定義します。 

    次の点に注意してください。 

    承認済みのJavaScript生成元 – 呼び出し元のページに対し、ローカルで作成されたスクリプトのみを許可します。 

    承認済みのリダイレクトURI – 理論上はクライアントアプリケーションを任意のサイトにリダイレクトできますが、InterSystems IRISのOAUTH実装を使用する場合は https://localhost/csp/sys/oauth2/OAuth2.Response.cls にリダイレクトする必要があります。 スクリーンショットのように複数の承認済みのリダイレクトURIを定義できますが、このデモでは2つのうち2番目のエントリのみが必要です。 

    最後に、InterSystems IRISをGoogle認証サーバーのクライアントとして構成する必要があります。 

    Cachéの構成 

    InterSystems IRIS OAUTH2クライアントの構成は2段階で行われます。 まず、サーバー構成を作成する必要があります。 

    SMPで、System Administration(システム管理) > Security(セキュリティ) > OAuth 2.0 > Client Configurations(クライアント構成)を開きます。 

    「サーバー構成の作成」ボタンをクリックし、フォームに入力して保存します。 

    フォームに入力したすべての情報は、Google Developers Consoleのサイトで確認できます。 InterSystems IRISはOpen IDの自動検出に対応しています。 ただし、ここでは自動検出を使用せず、すべての情報を手動で入力しました。 

    次に、新しく作成された発行者エンドポイントの横にある「Client Configurations」(クライアント構成)リンクをクリックし、「Create Client Configuration」(クライアント構成を作成する)ボタンをクリックします。 

    「Client Information」(クライアント情報)タブと「JWT Settings」(JWT設定)タブは空のままにし(デフォルト値を使用します)、クライアントの認証情報を入力してください。 

    注意:ここでは、Confidential Clientを作成しています。これはPublic Clientよりも安全であり、クライアントシークレットがクライアントサーバーアプリケーションを離れることはありません(ブラウザに送信されません)。 

    また、「Use SSL/TLS」(SSL/TLSを使用する)がチェックされ、ホスト名(ここではクライアントアプリケーションにローカルにリダイレクトしているため、localhostにします)が入力され、さらにはポートとプレフィックスが入力されていることを確認してください(これは同じマシンに複数のInterSystems IRISがある場合に役立ちます)。 入力した情報に基づいてクライアントリダイレクトURLが生成され、上の行に表示されます。 

    上のスクリーンショットでは、GOOGLEという名前のSSL構成を選択しました。 この名前自体は、多くのSSL構成のうちどれをこの特定の通信チャネルで使用するかを決定するためにのみ使用されます。 CachéはSSL/TLS構成を使用し、サーバー(この場合はGoogle OAuth 2.0 URI)との安全なトラフィックを確立するために必要なすべての情報を保存しています。 

    より詳細な説明については、ドキュメントを参照してください。 

    Googleの認証情報定義フォームから取得したクライアントIDとクライアントシークレットの値を入力します(手動構成を使用する場合)。 

    これですべての構成ステップが完了し、CSPアプリケーションのコーディングに進むことができます。 

    クライアントアプリケーション 

    クライアントアプリケーションは、シンプルなWebベースのCSPアプリケーションです。 そのため、Webサーバーによって定義および実行されるサーバー側のソースコードと、Webブラウザによってユーザーに公開されるユーザーインターフェイスで構成されています。 

    クライアントサーバー 

    クライアントサーバーは単純な2ページのアプリケーションです。 アプリケーション内では次の処理を実行します。 

    ·        Google認証サーバーのリダイレクトURLを組み立てます。 

    ·        Google Drive APIおよびGoogle Calendar APIへのリクエストを実行し、結果を表示します。 

    ページ1 

    これはアプリケーションの1ページであり、そのリソースについてGoogleを呼び出すことにしました。 

    以下はこのページを表す最小限の、しかし完全に動作するコードです。 

    Class Web.OAUTH2.Google1N Extends %CSP.Page 
    
    { 
    
    
    Parameter OAUTH2CLIENTREDIRECTURI = "https://localhost/csp/google/Web.OAUTH2.Google2N.cls"; 
    
    
    Parameter OAUTH2APPNAME = "Google"; 
    
    
    ClassMethod OnPage() As %Status 
    
    { 
    
      &html<<html> 
    
    <head> 
    
    </head> 
    
    <body style="text-align: center;"> 
    
            <!-- ページの内容をここに挿入します --> 
    
            <h1>Google OAuth2 API</h1> 
    
            <p>このページのデモでは、OAuth2認証を使用してGoogle API関数を呼び出す方法を示しています。 
    
            <p>ユーザーとユーザーのGoogleドライブのファイル、およびカレンダーエントリに関する情報を取得します。 
    
            > 
    
             
    
      // Googleで認証するにはopenidのスコープを指定する必要があります 
    
      set scope="openid https://www.googleapis.com/auth/userinfo.email "_ 
    
      "https://www.googleapis.com/auth/userinfo.profile "_ 
    
      "https://www.googleapis.com/auth/drive.metadata.readonly "_ 
    
      "https://www.googleapis.com/auth/calendar.readonly" 
    
    
      set properties("approval_prompt")="force" 
    
      set properties("include_granted_scopes")="true" 
    
    
      set url=##class(%SYS.OAuth2.Authorization).GetAuthorizationCodeEndpoint(..#OAUTH2APPNAME,scope, 
    
        ..#OAUTH2CLIENTREDIRECTURI,.properties,.isAuthorized,.sc)  
    
      w !,"<p><a href='"_url_"'><img border='0' alt='Googleサインイン' src='images/google-signin-button.png' ></a>"  
    
    
          &html<</body> 
    
    </html>> 
    
      Quit $$$OK 
    
    } 
    
    
    ClassMethod OnPreHTTP() As %Boolean [ ServerOnly = 1 ] 
    
    { 
    
      #dim %response as %CSP.Response 
    
      set scope="openid https://www.googleapis.com/auth/userinfo.email "_ 
    
        "https://www.googleapis.com/auth/userinfo.profile "_ 
    
        "https://www.googleapis.com/auth/drive.metadata.readonly "_ 
    
        "https://www.googleapis.com/auth/calendar.readonly" 
    
      if ##class(%SYS.OAuth2.AccessToken).IsAuthorized(..#OAUTH2APPNAME,,scope,.accessToken,.idtoken,.responseProperties,.error) { 
    
        set %response.ServerSideRedirect="Web.OAUTH2.Google2N.cls" 
    
      } 
    
      quit 1 
    
      } 
    
    } 

    以下にこのコードの簡単な説明を記します。 

    1.     OnPreHTTPメソッド - まず、すでに有効なアクセストークンをGoogleの認証結果として取得しているかどうかを確認します。この認証は、例えば単にページを更新したときに発生する可能性があります。 トークンを取得できていない場合は、認証する必要があります。 トークンを取得できている場合は、結果表示ページにページをリダイレクトするだけです。 

    2.      OnPageメソッド - 有効なアクセストークンがない場合にのみここに到達します。その場合、認証してGoogleに対する権限を付与し、アクセストークンを付与してもらうために通信を開始しなければなりません。 

    3.      Google認証ダイアログの動作を変更するスコープ文字列とプロパティ配列を定義します(私たちのIDに基づいて認証する前に、Googleに対して認証する必要があります)。 

    4.      最後にGoogleのログインページのURLを受け取り、それをユーザーに提示してから同意ページを表示します。 

    追加の注意事項: 

    ここでは実際のリダイレクトページを https://www.localhost/csp/google/Web.OAUTH2.Google2N.cls(OAUTH2CLIENTREDIRECTURIパラメータ内)で指定しています。 ただし、Google認証情報の定義ではInterSystems IRIS OAUTH Frameworkのシステムページを使用しています。 リダイレクトは、OAUTHハンドラークラスによって内部的に処理されます。 

    ページ2 

    このページにはGoogle認証の結果が表示されます。成功した場合はGoogleのAPIコールを呼び出してデータを取得します。 繰り返しになりますが、このコードは最小限でも完全に機能します。 受信データがどのような構造で表示されるかは、皆さんのご想像にお任せします。 

    Include %occInclude 
    
    
    Class Web.OAUTH2.Google2N Extends %CSP.ページ 
    
    { 
    
    
    Parameter OAUTH2APPNAME = "Google"; 
    
    
    Parameter OAUTH2ROOT = "https://www.googleapis.com"; 
    
    
    ClassMethod OnPage() As %Status 
    
    { 
    
      &html<<html> 
    
       <head> 
    
       </head> 
    
       <body>> 
    
    
      // アクセストークンがあるかどうかを確認します 
    
      set scope="openid https://www.googleapis.com/auth/userinfo.email "_ 
    
        "https://www.googleapis.com/auth/userinfo.profile "_ 
    
        "https://www.googleapis.com/auth/drive.metadata.readonly "_ 
    
        "https://www.googleapis.com/auth/calendar.readonly" 
    
    
      set isAuthorized=##class(%SYS.OAuth2.AccessToken).IsAuthorized(..#OAUTH2APPNAME,,scope,.accessToken,.idtoken,.responseProperties,.error) 
    
      if isAuthorized {  
    
        // Googleにはイントロスペクションエンドポイントがありませんので、呼び出す必要はありません。イントロスペクションエンドポイントと表示結果については、RFC 7662を参照してください。   
    
        w "<h3><span style='color:red;'>GetUserInfo API</span>からのデータ</h3>" 
    
        // userinfoには専用のAPIがありますが、Get() メソッドを適切なURLで呼び出すだけでも取得できます。     
    
        try { 
    
        set tHttpRequest=##class(%Net.HttpRequest).%New() 
    
          $$$THROWONERROR(sc,##class(%SYS.OAuth2.AccessToken).AddAccessToken(tHttpRequest,"query","GOOGLE",..#OAUTH2APPNAME)) 
    
          $$$THROWONERROR(sc,##class(%SYS.OAuth2.AccessToken).GetUserinfo(..#OAUTH2APPNAME,accessToken,,.jsonObject)) 
    
          w jsonObject.%ToJSON() 
    
        } catch (e) { 
    
          w "<h3><span style='color: red;'>エラー: ",$zcvt(e.DisplayString(),"O","HTML")_"</span></h3>"     
    
        } 
    
    
        /****************************************** 
    
        *                                         * 
    
        *      他のAPIから情報を取得する      * 
    
        *                                         * 
    
        ******************************************/ 
    
        w "<hr>" 
    
    
        do ..RetrieveAPIInfo("/drive/v3/files") 
    
      
    
        do ..RetrieveAPIInfo("/calendar/v3/users/me/calendarList") 
    
    
      } else { 
    
        w "<h1>認証されていません!</h1>"   
    
      } 
    
      &html<</body> 
    
      </html>> 
    
      Quit $$$OK 
    
    } 
    
    
      
    
    
    ClassMethod RetrieveAPIInfo(api As %String) 
    
    { 
    
      w "<h3><span style='color:red;'>"_api_"</span>からのデータ</h3><p>" 
    
      try { 
    
        set tHttpRequest=##class(%Net.HttpRequest).%New() 
    
        $$$THROWONERROR(sc,##class(%SYS.OAuth2.AccessToken).AddAccessToken(tHttpRequest,"query","GOOGLE",..#OAUTH2APPNAME)) 
    
        $$$THROWONERROR(sc,tHttpRequest.Get(..#OAUTH2ROOT_api)) 
    
        set tHttpResponse=tHttpRequest.HttpResponse 
    
        s tJSONString=tHttpResponse.Data.Read() 
    
        if $e(tJSONString)'="{" { 
    
          // JSONではない 
    
          d tHttpResponse.OutputToDevice() 
    
        } else {       
    
          w tJSONString 
    
          w "<hr/>" 
    
          /* 
    
          // 新しいJSON API 
    
          &html<<table border=1 style='border-collapse: collapse'>> 
    
          s tJSONObject={}.%FromJSON(tJSONString) 
    
          set iterator=tJSONObject.%GetIterator() 
    
            while iterator.%GetNext(.key,.value) { 
    
              if $isobject(value) { 
    
                set iterator1=value.%GetIterator() 
    
                w "<tr><td>",key,"</td><td><table border=1 style='border-collapse: collapse'>" 
    
                while iterator1.%GetNext(.key1,.value1) { 
    
                if $isobject(value1) { 
    
                    set iterator2=value1.%GetIterator() 
    
                    w "<tr><td>",key1,"</td><td><table border=0 style='border-collapse: collapse'>" 
    
                    while iterator2.%GetNext(.key2,.value2) { 
    
                        write !, "<tr><td>",key2, "</td><td>",value2,"</td></tr>"                    
    
                    } 
    
                    // このようにして埋め込みオブジェクト/配列をどんどん進めていきます 
    
                  w "</table></td></tr>" 
    
                } else { 
    
                      write !, "<tr><td>",key1, "</td><td>",value1,"</td></tr>"        
    
                } 
    
                } 
    
              w "</table></td></tr>" 
    
              } else { 
    
                  write !, "<tr><td>",key, "</td><td>",value,"</td></tr>" 
    
              } 
    
            }        
    
        &html<</table><hr/> 
    
        > 
    
        */ 
    
        } 
    
      } catch (e) { 
    
        w "<h3><span style='color: red;'>エラー: ",$zcvt(e.DisplayString(),"O","HTML")_"</span></h3>" 
    
      } 
    
    } 
    
    
    } 

      

    コードを簡単に見てみましょう。 

    1.      まず、有効なアクセストークンがあるかどうかを確認する必要があります(そのため、認証を受けました)。 

    2.      トークンがある場合はGoogleが提供し、発行されたアクセストークンがカバーするAPIにリクエストを発行できます。 

    3.       そのためには標準の %Net.HttpRequest クラスを使用しますが、呼び出されたAPIの仕様に従ってGETメソッドまたはPOSTメソッドにアクセストークンを追加します。 

    4.       ご覧のように、OAUTHフレームワークには GetUserInfo() メソッドが実装されていますが、RetrieveAPIInfo() ヘルパーメソッドの場合と同様に、Google APIの仕様を利用して直接ユーザー情報を取得できます。 

    5.       OAUTHの世界ではJSON形式でデータを交換するのが一般的であるため、ここでは受信データを読み取り、それを単にブラウザに出力してます。 受信データを解析して整形し、それをユーザーが理解できる形で表示できるようにするのはアプリケーション開発者の責任です。 しかし、それはこのデモの範囲を超えています。 (ただし、いくつかのコメントアウトされたコードで構文解析のやり方を示しています。) 

    以下は、未加工のJSONデータが表示された出力のスクリーンショットです。 

    パート2に進んでください。そこでは、認証サーバーおよびOpenID Connectプロバイダーとして機能するInterSystems IRISについて説明します。 

    [1]https://tools.ietf.org/html/rfc6749https://tools.ietf.org/html/rfc6750 

    0
    0 857
    記事 Shintaro Kaminaka · 7月 3, 2020 8m read

    IRIS 2019.4以降の製品には、Prometheus形式でIRISのメトリックを公開する/api/monitorサービス機能が実装されています。 IRISのメトリックを監視・警告ソリューションの一部として使用したい人にとっては大きなニュースです。 このAPIは、IRISの次期バージョンでリリースされる予定の新しいIRIS System Alerting and Monitoring (SAM) ソリューションのコンポーネントです。 

    ただし、IRISインスタンスを監視するためにSAMがこのAPIの計画と実証実験を開始するのを待つ必要はありません。 今後の投稿では利用可能なメトリックとその意味についてさらに掘り下げ、対話型ダッシュボードの例を示します。 しかし、まずは背景の説明といくつかの質問と回答から始めましょう。 

    0
    0 401
    記事 Shintaro Kaminaka · 5月 1, 2020 14m read

    この記事では、REST API開発への仕様ファーストアプローチについて説明します。 

    従来のコードファーストREST API開発は次のようになります。 

    • コードを書く 
    • RESTを有効にする  
    • ドキュメント化(REST APIとして) 

    仕様ファーストのアプローチでは同じ手順を行いますが、順序が逆になります。 ドキュメントを兼ねた仕様書を作成し、そこからRESTアプリの定型文を生成して、最後にビジネスロジックを書きます。

    これは、次の理由でメリットがあります。 

    0
    0 1310
    記事 Shintaro Kaminaka · 4月 28, 2020 9m read

    CachéまたはEnsembleへの接続にStudio、ODBC、またはターミナルを使用している場合、その接続をどのように保護すれば良いのか疑問に思うかもしれません。 選択肢の一つに、TLS(別名SSL)を接続に追加することが挙げられます。 Cachéクライアントアプリケーション(TELNET、ODBC、Studio)にはすべて、TLSを接続に追加する機能があります。 あとは単純にその構成を行うだけです。 

    2015.1以降はこれらのクライアントを簡単に設定できるようになりました。 ここでは、その新しい方法について説明します。 既に古い方法を使用している場合も引き続き機能しますが、新しい方法への切り替えを検討することをお勧めします。 

    背景 

    これらのクライアントアプリケーションは、サーバーがインストールされていないマシンにインストールできます。 ただし、CACHESYSデータベースやcpfファイルなど、設定を保存する通常の場所へのアクセスに依存することはできません。 その代わり、受け付ける証明書やプロトコルの設定はテキストファイルに保存されます。 このファイルの設定の多くは、管理ポータルのSSL/TLS構成の設定に似ています。 

    設定ファイルはどこにありますか? 

    独自のファイルを作成する必要があります。 クライアントインストーラーは設定ファイルを作成しません。 

    0
    0 621