FileMakerの集計の結果をテキストで得る方法

FileMakerの集計は、便利に使えるときもありますが、ちょっと柔軟性が足りないと思うときもありなかなか使いこなすのが難しい機能でもあります。ここで説明するのは、集計結果をテキストファイルで出力したり、JSONで得るなどの処理をするための基本的な方法です。FileMakerのスクリプトだけで実現でき、十分に速く処理できる方法なので、記事として公開してみました。FileMakerの基本や集計の一般的なことをご存知という前提で、この記事は進めます。

まず、背景ですが、実際のデータベースで説明します。次のような、タイムスタンプと「金額」のフィールドがあるようなデータベースがあるとします。話を簡単にするために、ごくシンプルなデータベースで説明します。「年月」フィールドは計算フィールドで、「日時」フィールドより求めています。この先やりたいことは、『月ごとの金額の合計を求める』ということです。ということで、金額フィールドの合計を求める集計フィールド「金額の合計」も定義しました。

ここから、新しいレイアウトを作ります。詳細は省略しますが、年月フィールドでソートした場合に表示される小計パートをボディパートの上に設置し、そこには「年月」および「金額の合計」フィールドを配置します。以下の図では、小計パートは薄いグレーの背景です。白背景がボディパートで、「年月」「金額」フィールドが配置されています。一応、全レコードを表示した上でソートして、以下のように集計が出ています。

ここで、「各月の集計結果だけが欲しい」という要求が発生しました。もちろん、上記レイアウトのボディパートを消せばいいですね(消さないで見えなくすることができればいいのにと思いながら)。消したら、もちろん、小計パートだけが見えます。

これでOKと思ったら、「この画面の結果をテキストファイルで欲しい」という要求がさらに出ました。最も、最初から集計結果のテキストだけ欲しいというのなら、集計フィールドを使わないで、FileMaker以外の手段を考えるかもしれませんが、集計レイアウトを作る上で、さらにテキストも欲しいということになったとしてください。

では、このままエクスポートしても、3行のテキストファイルは作られません。エクスポートの中身はボディパートに展開された集計元のレコードです。もちろん、「金額の合計」フィールドには金額の合計はあるのですが、1ヶ月分でも何百のレコードがありそうです。手で消すのもちょっと無理があります。

もしかしたら、以下のようなスクリプトでなんとかなるでしょうと思う人もいるかもしれません。つまり、レイアウトで最初のレコードから最後のレコードまでグルグル回してフィールド値を変数に取り込むのです。

このスクリプトを実行した結果、$result変数は、こんな感じ。多分、レコード数の1000だけあって、最初のいくつかだけが見えています。細かいことはともかく、レイアウトに見える結果と大きく違います。

この方法でのスクリプトは、ボディパートを順番に舐めて行くのであって、小計パートは関係ありません。残念ながら小計パートを選択するということがFileMakerではできないのであって、この方法ではどうすることもできません。もっとも、「年月」フィールドの変わり目を…とやったらできるのでしょうけど、この方法で、数万くらいのレコードになると、そこそこ時間がかかります。データが多いと現実的ではありません。

しかしながら、ちょっとした発想の転換で、同じような方法ながら、小計に見えている数値だけを取り出すスクリプトが書けました。次のようなスクリプトで、結果として見えるダイアログボックスも紹介します。ダイアログボックスは、集計レイアウトの小計パートに見える「金額の合計」フィールドの数値が見えています。

ここでは改行付きのテキスト(リスト形式)で取り出しましたが、変数をそのままテキストファイルに書き出すこともできますし、JSON形式で取り出すということもできます。とにかく、小計パートでのフィールドあるいは集計フィールドだけを取り出せるということがポイントなので、細かくはソリューションに合わせて対処してください。

このスクリプトのポイントは、「対象レコードの絞り込み」スクリプトステップを利用するということです。なお、レコードが0になるまで省略を続けるので、「エラー処理」スクリプトステップで、「レコードがない」というエラーを出ないようにしておきます。なお、前述のスクリプトは、最初からレコードがない場合にはいい感じでは動いてくれないと思いますので、データがあること前提です。デバッガでステップ動作していただければ大体わかると思いますが、このスクリプトは次のような流れを想定しています。

  1. 先頭のレコードを選択する。これは「年月」フィールドの最初のグループ(202503)に属するレコードなので、そのレコードの「金額の合計」フィールドは集計されて、202503の合計金額(49734287797)が得られる
  2. 取り出したレコードの所属するグループのレコードを全部省略する。つまり、ここでは年月が202503のレコードを省略する。そのために「対象レコードの絞り込み」スクリプトステップを利用する
  3. 最初のレコードに移動すると、レイアウトでは2つ目に見えていた「年月」フィールドが202504のグループのレコードになる。「金額の合計」フィールドから、202504の金額の合計値が得られる
  4. レコードの絞り込み後レコードがなくなるまで2,3を続ける

レコードを移動するのは諦めて、先頭のレコードから取り出せるものをうまく利用します。データを取り出したら、そのグループを集計対象外、つまり省略してしまうのです。いろんなやり方はあるでしょうけど、「対象レコードの絞り込み」スクリプトステップが便利です。変数$ymに現在の「年月」フィールドの値を入れておいて、検索条件を組み立てます。このように必ず6桁の数値になるようなフィールドの検索では==をつけるのはちょっとやりすぎかもしれませんが、念の為ということで。ポイントは、処理として「レコードを対象外に」を選択することです。つまり、条件に合ったレコードを対象外にして、集計対象から排除するのです。

このような処理は非常に遅いのでは無いかと危惧するかもしれませんが、結構早いです。集計時に一旦レコードを全部ロードしていて、対象外にする処理では、メモリの移動や増加、ネットワーク処理などは入らないからでしょう。すぐに終わります。10万件で試してみても、数秒もかからずに終わりました。

小計パートが2つある場合でも、同様にできました。ただし、レコードを対象外にする場合には、2つの小計パートのソート対象フィールドに対して、ANDで検索をかけます。ということで、ちょっとずつレコードを省略して最初のレコードの集計値を取り出せば良いので、小計パートがいくつになっても大丈夫です。

ちなみに、レコードの省略では、対象レコードが増えないので、ソート結果はそのまま保持されるという動作もこの方法での集計結果の取得を実現する仕様の1つです。

集計結果だけのテキストを得るにはいろんな方法がありますが、この方法は、FileMakerの中だけで完結しているというところが大きなポイントでしょう。外部にCSVを出すなどして処理するのは別システムの管理がかえって面倒になりますし、後々のメンテナンスを考えれば、システム基盤はシンプルな方が良いということになります。ちなみに、この仕組みがどんなところで必要になったかと言うと、複数の異なるテーブルの集計結果を1つの表にまとめたかったからです。ただ、その纏める部分は色々考えて、集計結果をJSONで得て、WebビューでJavaScript処理で表を作るということで解決しました。

テストしたデータベースも公開します。レコードは10万くらいにしてあります。あまり、使い勝手は良くないですが、ご了承ください。

GitHubに登録するDockerコンテナの設定に関して

ブログを個人のメモ代わりにしている人も多いと思う。であれば、いきなり内容を書いてもいいのだが、あまりに省略してしまうのも、公に晒すドキュメントとしての体をなさない気もするので、それなりに真面目に書こうと思う。ということで、このテーマはかなり重箱の隅テーマでもあるが、状況からスタートだ。

私は、Webアプリケーションのフレームワーク「INTER-Mediator」を開発している。なるべくシンプルにアプリケーション開発が可能なような仕組みをフレークワークとして提供している。例えば、HTMLページで<input data-im=”item@name”>とすれば、このテキストフィールドにitemテーブルのnameフィールドの値が表示され、変更すれば自動的にフィールドがアップデートされるといった仕組みだ。もちろん、もっと色々あるが、それはサイトを見ていただくとしよう。このINTER-Mediatorを試してみるための素材として、本体とは別にレポジトリを提供している。これを以下、「トライアル」と呼ぶ。トライアルを稼働させれば、INTER-Mediatorのコードの一部で提供しているサンプルのページを稼働させてみることができる。さらにはチュートリアルの演習を進める基盤としても稼働できるようになっている。このトライアル自体を容易にセットアップできるようにするために、Dockerの素材をレポジトリに提供している。このレポジトリをダウンロードして、レポジトリのルートで「docker compose up -d」などとすれば、ちょっと待てばサンプルアプリやチュートリアルを進めるための仕組みが稼働して、「http://localhost:9080」にブラウザから接続すれば良いという仕掛けである。

トライアルのDockerコンテナ作成は、当初はいわゆるDockerfileに不足するPHPライブラリのインストールやビルド等を含めたものを作っていた。php-apacheというコンテナをもとにしているが、結構必要なものを削ってあるので、追加のセットアップが必要なのである。そのビルドに結構時間がかかるというのも問題だったのだが、ある時から、そのセットアップが途中で落ちるようになってきた。再度、docker compose …とやれば落ちたところをやり直して先に進むのだが、これではちょっと使う側としては心許ないだろうから、なんとかしないといけないということになった。そこで、php-apacheに加えて、INTER-Mediatorを稼働するのに必要なphpの動作基盤を作って、DockerfileのFROMには必要なライブラリが含まれたphp-apacheを利用するようにすることで、スピードおよびビルドの失敗に対して対処できると考えた。そして、その基盤となるコンテナを、GitHub Packegesで配布することにし、アクションを定義するなどした。名付けてphp-apache_imである。imと名前に入っているがINTER-Mediator自体は入っていない。INTER-Mediatorはcomposer.jsonファイルに定義して、composerでインストールするようにすることを想定している。

まず、自分のアカウントmsykに、INTER-Mediatorのクローンがあるので、そこでアクションを動かして、マニュアル実行するようにしてパッケージを作ってみた。実際に動くアクションを紹介してもらい(松尾さん、Thanks!)それをもとにアクションを作りつつ、Dockerfileを構築して、思いの外、すぐにできてしまった。そして、トライアルのdocker-compose.ymlファイルで、自分のアカウントのパッケージを指定すると、数回程度のトライでうまく行った! これで、トライアルの構築は早くて安定するということだ。しかしながら、INTER-Mediatorのためのコンテナでもあるので、INTER-Mediatorのレポジトリに置きたい。このレポジトリは、Organizationとして用意した側にある。Organizationでも同じだろうと思ったのが甘かった。ということで、ここから本題なのだが(まだ前振りだったのか!)、Organization側に作ったGitHub Packagesは、なぜか利用できないのである。アクションを動かしてのエラーは出なかった。しかし、トライアルでdocker compose …とやると、次のようなエラーが出た。検索に引っかかるように、とりあえず、そのまま掲載する。

% docker compose up -d
[+] Running 1/1
 ✔ db Pulled                                                                                                                                                3.5s 
[+] Building 1.1s (2/2) FINISHED                                                                                                            docker:desktop-linux
 => [php-apache_im internal] load build definition from Dockerfile_Prebuild                                                                                 0.0s
 => => transferring dockerfile: 270B                                                                                                                        0.0s
 => ERROR [php-apache_im internal] load metadata for ghcr.io/inter-mediator/php-apache_im:latest                                                            1.1s
------
 > [php-apache_im internal] load metadata for ghcr.io/inter-mediator/php-apache_im:latest:
------
failed to solve: ghcr.io/inter-mediator/php-apache_im:latest: failed to resolve source metadata for ghcr.io/inter-mediator/php-apache_im:latest: failed to authorize: failed to fetch anonymous token: unexpected status from GET request to https://ghcr.io/token?scope=repository%3Ainter-mediator%2Fphp-apache_im%3Apull&service=ghcr.io: 401 Unauthorized
msyk@msyk-macbook-pro IMApp_Trial % docker compose up -d
[+] Building 0.8s (2/2) FINISHED                                                                                                            docker:desktop-linux
 => [php-apache_im internal] load build definition from Dockerfile_Prebuild                                                                                 0.0s
 => => transferring dockerfile: 268B                                                                                                                        0.0s
 => ERROR [php-apache_im internal] load metadata for ghcr.io/inter-mediator/php-apache_im:latest                                                            0.7s
------
 > [php-apache_im internal] load metadata for ghcr.io/inter-mediator/php-apache_im:latest:
------
failed to solve: ghcr.io/inter-mediator/php-apache_im:latest: failed to resolve source metadata for ghcr.io/inter-mediator/php-apache_im:latest: failed to authorize: failed to fetch anonymous token: unexpected status from GET request to https://ghcr.io/token?scope=repository%3Ainter-mediator%2Fphp-apache_im%3Apull&service=ghcr.io: 401 Unauthorized

ちなみに、ghcr.io/inter-mediator/php-apache_im:latestが、INTER-Mediatorレポジトリにあるパッケージを参照する識別子である。私のレポジトリに作ったものは、2つ目が「msyk」になっており、そこの部分をINTER-Mediator本体レポジトリに変えたものである。エラーは長いが、ポイントは、最後の「401 Unauthorized」だ。認証されていないのである。

そして、色々なサイトをチェックしてみた。このエラーについての直接的な言及はなかったのだが、あるサイト(情報提供に感謝です!)で、作成されたパッケージは「private」になっているという情報を得た。そうか、アクセス権かと思ったけど、じゃあどこで設定するのかとなる。それに応えるGitHubのページを見つけてなんとか進めることができた。しかし、その操作はかなりトリッキーだと思う。GitHubはいろんな機能があって助かる一方、セットアップはダンジョンでもある。それを記録するのがこのブログの目的だ。

その設定だが、以下のように行う。まず、パッケージは、レポジトリのルートのページで、右下にPackagesとあるので、そこをクリックすると、登録されているパッケージが出てくる。パッケージの項目をクリックするとパッケージの詳細が表示される。このページの右下にPackage settingsというリンクがあるので、それをクリックする。

開いたページの下の方に、Danger Zoneとまとめられた項目がある。ここで、Change visibilityをクリックする。

すると、次のようなパネルが開く。ああ、ここかと安心はできない。なんと、Publicは選択できないようになっている。画面に見えているが、組織管理者によってこの設定はできないようになっている。管理者って誰?俺じゃんってことで、自分でやらないといけないわけだ。

ということで、前に示したGitHubのページに従って作業をする。右上に自分の顔写真が見ている箇所をクリックして、Settingsの項目をクリックし、さらに左側でPackagesを選択する。これで設定できると思いきや、ここには設定はない。

ここで、自分の名前の下あたりにあるSwitch settings contextをクリックして、組織の項目を選択する。つまり、自分の設定ではなく、組織の設定を変更するという操作をしなければならない。

そして左側でPackagesを選択すると、Package creationの下に、Publicなどのチェックが見える。つまり、作ったパッケージの可視性として、PublicやInternalを選択できるようにするチェックボックスがある。ややこしいが、ここでPublicをチェックすると、前に示したDanger ZoneのVisibilityとしてPublicを選択できるようになる。

ちなみに、私のレポジトリに作ったパッケージの可視性の初期値はpublicだった。つまり、個人のレポジトリに作ったものは最初からpublic、組織のレポジトリに作ったものは最初はprivateだが、publicにするには変更が必要だったということである。なんか、細かいところだが、色々考えてのことだとは思うが、なかなかわかりづらいところでもある。

プログラミングはAIが取って代わる?に関して

LLMが世間に知られてから、AIが爆発的に普及しているのはすでに知られている通りです。特に、プログラミングの世界は、ChatGPTでプログラム作成しているという人がそこそこ出てきており、進展が早いジャンルになっています。これも、たまたまプログラマが普段からコンピュータを使って…というよりも、世間に普及したAIがプログラミングに対する生成機能等をしっかりと持っていたということの方が大きな要因でしょう。いずれにしても、ちょっとしたプログラムから、そこそこの規模のものまで、AI支援でプログラムが作られていますが、それを見て「プログラマは不要になる」と考えるのはちょっと性急ではないかと思います。

このプログラミングにAIの波は、80年代から90年代の「文字入力ムーブメント」に似た側面があると思います。文字入力ムーブメントの本質の1つはキーボードで、ローマ字で入力するということ、そしてもう1つの本質はかな漢字変換を使うことが大きな特徴です。要するに、文字を書くのではなく、入力するようになったのです。当時も、コンピュータ業界はもう手書きなんてしなくていいということでポジティブに見る反面、そうでない世界の人たちからは「漢字が書けなくなってみんな頭が悪くなる」などとネガティブな意見があったのも事実です。しかし、データ化したテキストは、結果的にコピペや、通信転送による劣化の少なさなど、色々なメリットが具現化され、もはや、多くの日本人が文字をきちんと書けるのかどうかとなるとかなり怪しくなっています。しかし、文字が書けなくても読めて入力できれば、多くの場面で問題なくなってきています。もちろん、文字を書くというトレーニングはどの世代でもある程度やってきているので、全く書けないわけではないでしょうけど、たとえば文章を作る場合、まずは手書きで…なんてことはもうしないという状況です。もっとも、これもAIによっていきなり下書き作らせるようになるほどに進化は止まりません。

ということで、漢字をとっさに書けない私たちでも、日々、文章を書いているということは、そのほかの色々なメリットを含めて悪いことではなかったということです。おそらく、AIによるプログラミングも同じことで、AIをうまく使うというノウハウの蓄積により、過去よりも効率的に開発ができるようになるという路線がすでに先の方まで見えているのが現状でしょう。もちろん、未来にも、いきなり for ( int i = 0 … などと書きじめることはなくはないでしょうけど、むしろ「繰り返して配列の要素の最大値を返して」とお願いして勝手にコードが書かれていたり、あるいはもっと大きな要求を入力して、長ーいプログラムの一部にforが書かれていたりというようなことに、もうすでになっているわけです。つまり、プログラマとしては、スクラッチから書けなくもないものの、fをキータイプすることからスタートするだけがプログラミングではないということになっているわけです。まあ、forの書き方くらいは忘れないかもしれませんが、フレームワークのAPIの引数指定などを正確に書いていかなければならなかった過去に比べて、そういう部分も大まかな指示をもとに正確にAIがコード生成します。それは、あたかも、漢字をちゃんと書けるか怪しい私たちでも文章が作れていくのと良く似ています。

では、AIプログラミングでは何が人間に残るのかとなると、よく言われているのは要求をまとめる作業があります。まあ、そうですね、人間が何か要求しないといけませんし、その要求をソフトウェアとして実現するお膳立ては人間がやることになるでしょう。ここもAIにさせられるだろうと思うかもしれませんが、それは続く話と関わりがあります。要求をまとめる以外に、AIが生成したコードを読み取って評価し、ビルドした結果を評価して使い物になるかどうかを判断し、そして軌道修正を行なって完成に持ち込むといったコーディング自体を読み取って判断し、それを完成品に繋げる能力も人間には必要でしょう。AIがそれらしいプログラムをだだーっと出して、「はい完成OK」というのはあるかもしれませんが、おそらく、世の中はそんなに単純ではないと思われます。文字の入力がキーボードやかな漢字変換で楽ちんになったとは言え、執筆や遂行という作業は効率は良くなったとしても、人間の頭が介在することにより、読むに絶えない文章になるか、素晴らしい文章になるかの分かれ道を突き進むのです。これと同じで、AIが生成したコードをきちんと評価するのは人間の仕事としては必ず残ります。もちろん、それ自体もAIの支援は期待できるのですが、完成品にいたるまでには、必ず人間の判断を的確に入れていくことが必要になるでしょう。そして、コーディング前の要求段階でもその点は同じと考えます。しかし、今はまだ、何をAIにさせて、何を人間がやればいいのかということが、あまりいい感じに言語化できてません。私も、Windsurfでアプリ生成ができるようにプロンプトなんかを開発したりして、見かけ上は、結構、なんでも自動的にできる感じにして、人々に見せるということをやりがちで、さも、問題はありません的な押し出しをしてしまいます。しかしながら、今は人間がやるべきことと、AIにやってもらって問題がないところの線引きが必要な段階になっているのではないでしょうか。

新しいものが出てくると、殊更よさを強調するあまり、行きすぎた結論を出すこともよく見られます。パソコン黎明期の頃は、作曲はDJのソフトのレビューの最後に「これを使えば誰でもミュージシャンになれる」みたいな締めをしていた雑誌も散見されました。ちなみに、流石にワープロのレビューで「これを使えば誰でも作家になれる」とまでは流石に言わなかったのですが、アート系を舐めてはないと思っていました(と言いつつも自分で書いた原稿ではそんなこと書いてないつもりでも、編集部で直されているかもしれないですが)。今は、AIについては、まさに「なんでもできる」的な見方もされていません。もちろん、過去にはできなかったようなことまで手を広げられることも事実ですが、過去に鑑みて、冷静にできることとできないことを見極めて行くことが求められているのではないでしょうか。

そして、今から心配しているのがセキュリティのことです。生成AIを経由することで、簡単にはわからない形で自分のシステムに攻撃が来るのではないかということです。実はもう、すでに攻撃されているかもしれません。生成AIのモデルを信じるのか、あるいは信じないのか、さらに生成AIのモデルを掻い潜ってアタックがやってくるメカニズムが存在するのかしないのか、まだ、全くわかりません。過去、いろんなセキュリティ上の問題が私たちに降りかかってきていますが、それは当初は予測できなかったから問題になっているわけです。生成AIについても、同じような危険があるのではと考えるのは自然ではないでしょうか。だからと言って、具体的にどうするのかということは難しいですが、心の片隅には留めたいことです。

Webアプリを自動生成、ここまでできる!

INTER-MediatorはWebアプリケーションを作成できるフレームワークですが、それなりに容易に開発できるように作ってあるものの、それでも敷居は高いと言われることも多いです。理想は「〜をしたい」と要求を入れれば、アプリケーションを生成することです。遠い先か、生きている間は無理かと思っていたのですが、昨今のLLMを中心としたAIの発展でそこそこ実現できそうです。そして、AIの潮流にINTER-Mediatorも乗ることできそうです。以下に示すように、作りたいアプリケーションをテキストで与えれば、INTER-Mediatorのアプリケーションを生成できます。ツールは、Windsurfを使っています。

ただし、単に「言うだけ」と言うのは流石に無理です。超マイナーフレームワークのINTER-Mediatorのことは、相対的には少ない情報しかインターネットで得られないものなので、モデルに正確な情報が含められていない感じです。つまり、生で使ってもうまく行かないので、どうにかすると言うことで、INTER-Mediator向けの追加のプロンプトを作ってみました。それは、こちらのサイトでご覧いただけますが、このプロンプトはまだレポジトリに組み入れる前なので、私がフォークしたレポジトリへのリンクになっています。このページにある「プロンプト例」の直下にある『住所録のWebアプリケーションをINTER-Mediatorを利用して作成してください。INTER-Mediatorについては以下の情報を参照してください。』が、Windsurfに与えるお題です。これに加えて、それ以下の長いテキストを与えます。要するに、プロンプト例より下のコードを、全部プロンプトとして与えます。上記リンクではMarkdown形式が整形されて見えていますが、実際にはソースのテキストをそのまま使うのが良いでしょう。そして、アプリケーションを作った結果が以下の通りです。

最初にプロンプトとそれに引き続いてINTER-Mediatorに関する追加情報をまとめて与えています。ちなみに、同じアカウントで何度も実行しているので、もしかすると、人によっては同じ結果ではないかもしれませんが、もともと、そこそこ問題なく進めていて、追加情報が必要そうなところでプロンプトを追加したものをまとめたのが長いテキストの部分です。概ね、進められると思います。

そして、SQLのスキーマ作成とその適用、Composerを使ったINTER-Mediatorのインストール、INTER-Mediator特有の追加のインストール作業、そして、lib/params.phpの作成と変数の変更とどんどんと自動で行くと思います。途中でコマンド実行を行うかを尋ねてくれば、ほぼ全部Acceptボタンを押せば大丈夫です。

そして、INTER-Mediatorのマスター/ディテール形式のページを運用できるように、定義ファイルとページファイルを作ります。このあたりは素で作る(つまり、追加プロンプトなしで作る)とかなりぐちゃぐちゃになりますが、何をすべきかをかなり詳細に追加のプロンプトで指定しているので、問題なく作成できます。

全部作成できたら、PHPがセットアップされていることを自動的に認識して、PHPのサーバモードでアプリケーションが動作します。ブラウザで接続すれば、ちゃんと動いている!!最初にレコードが入ってしまっているのは、その前に作った時のSQLiteのファイルがそのまま残ってしまっていて、それを使ったからで、普通はレコードがない状態になります。

と言うことで、まとめると、『住所録のWebアプリケーションをINTER-Mediatorを利用して作成してください。』とお願いすれば、こんなWebアプリケーションを作ってくれると言うところまで時代は進歩したと言うことです。いろんなパターンのプロンプトがあれば、便利そうですね。

なお、上記は、macOS 15.2で稼働しました。WindsurfはProプランに入っています。LLMは規定値のClaude 3.5 Sonnetをそのまま使っています。GPT 4oを選択できるのですが、こちらだと、コマンド等の自動入力はできず、実際にコマンドを入れたりするのは人間の側になります。

アクセス権セット内の計算式ではグローバル変数は使えない

FileMakerネタです。ログイン可能なアカウント作成はいいとして、自分で作ったレコードしか見えないようにしたいというのはニーズの多い仕組みです。結構いろんなことをしないと行けないのですが、その設定の肝となるは、アクセス権セットのレコードに対するアクセス権です。細かな設定はこの記事では説明しませんので、詳しい方はご安心ください。FileMakerのあの辺りの機能は、ダイアログボックスが開きまくる手間のかかるUIですので、ポイントだけ示します。

アクセス権セットの [データ入力のみ] の複製を作って、名前を付け替えるなどするのが最初の作業です。「入力のみ」という違和感ある名前はともかく、複製を作り、レコードのポップアップで、「カスタムアクセス権」を選択します。

すると、次のようなダイアログボックスが新たに開きます。ここで、該当するテーブルを選択して、リストの下に見えるポップアップメニューから「制限」を選択します。一般に、「表示」の設定を変えるのは必須ですが、「表示」で制限をかければ、編集/作成/削除に対しては何もできないようなので、全部の項目に設定は必要ありません。

この「制限」を選択すると、式を入力するダイアログボックスが表示されます。そこで、論理式を入力します。論理式がTrueなら表示可能ということで普通にレイアウトにレコードのデータが表示されます。論理式がFalseなら <アクセス権がありません> とフィールドに出ます。こうした表示するかどうかを、レコードごとに決めることができるというのが、カスタムレコードアクセス権の設定で実現します。

ここで対象となるテーブルに、テキスト型でusernameというフィールドを作っているとします。そして、そのフィールドには、レコード新規作成時に自動的に「GET ( アカウント名 ) 」という式の結果を入力します。「ユーザ名」ではなく「アカウント名」で、こちらがログインした認証情報に含まれるユーザ名になります。

そして、カスタムレコードアクセス権の制限の式に「username = GET ( アカウント名 ) 」という式を設定しておけばOKです。この辺り、私たちの書籍「FileMaker as a Relational Database」の「8.3 レコード単位のアクセス権」に記述があります。設定手順がちょっと込み入っとるやないか!と今見たら思ってしまいますが、基本はここで説明したものと同様、アクセス権設定に対するレコード単位のアクセス権指定を行っています。

さて、ここからが本番(長い)。ある案件で、単にユーザ単位でのレコードの分離(レコードごとのアクセス権設定を行なった状態)にするだけでなく、ユーザのグループを定義してグループだと、所属している全てのユーザのレコードが見えるようにするという要件がありました。ここでのグループは、OSのグループとは違って、形態としてはユーザです。要するに権限の高いユーザがグループであり、権限自体に段階があるというか、自分だけと、グループ全体の段階があるという状態です。

これを実現するために、2つの方法を考えました。1つは、ここでのユーザとグループそれぞれ別々のアクセス権セットを割り当てることです。もう1つは1つのアクセス権セットでの「制限」の論理式でうまく処理を組み込むということです。まあ、当然、後者の方が面白そうなので、まずはそちらでやってみました。なお、あるユーザがどのグループに所属するかを記録しないと行けないので、ユーザ管理のテーブルをFileMakerに作る必要があります。{主キー, username, password, is_group, groupname} がテーブル構成です。ユーザ名、グループ名、いずれもテキストで記録するようにします。ここでいちいち主キーでリレーションするのもやり過ぎ感があるので、テキストで十分です。グループは、is_groupがTrueにしておきます。そうすると、ユーザのレコードで所属グループの選択を行うときに、グループだけの(is_groupがTrueのレコードだけの)ポップアップ等が作れるので、このようにしました。このテーブルから、ユーザがどのグループに属するかがわかるという段取りです。passwordはいらんといえばいらんのですが、せっかくこんなテーブルを作るので、いちいち、セキュリティ管理のダイアログボックスを出さなくても、ボタン1つでユーザの作成や削除ができるようなUIを作りました。そのためにはパスワードも入力しておくのが便利です。

そして、大変重要なリレーションシップの設定が必要になります。あるテーブルにusernameフィールドを作って、そこにログインアカウント名を入力しておいて、誰のレコードなのかがわかるようになっているということになるのですが、さらにそのレコードはどのグループの所属なのかをわかるようにしなければなりません。つまり、テーブルから、前者のユーザテーブルのusernameに対してリレーションシップを定義し、ユーザテーブルのgroupnameとして、各レコードの所属グループが得られるようにしておく必要があります。

ここで、アクセス権セットの「制限」の式を構築するとき、要するにログインしているユーザがここでの普通のユーザの場合と、グループユーザの場合では、比較するフィールドが違ってくるということがあります。その処理をやりやすくするために、ログイン直後にユーザテーブルで自分自身のレコードを検索して、is_groupフィールドを見て、自分が通常のユーザなのか、グループユーザなのかを$$変数に置いておき、その変数値を使った計算式を立てれば良いのではと考えました。そして、「制限」の式に$$変数を使ったものを記述したのですが、画面上はなんとなく問題ないように見えるのですが、検索をしても1つもレコードが返ってきません。

なんでか? よく考えてみると、$$変数というか、変数ってコンテキスト依存のものです。つまり、画面に出て来る段階で適用されるようなものと考えて良いでしょう。検索で変数は使えますが、変数の値を求めてそれを条件としてサーバに送っていると考えられます。検索処理そのものは、サーバで行われるとすれば、サーバでレコードを探っている段階では変数の値は全部未確定と考えるべきなので、変数が全部未定義でfalseになるのではと思われます。とは言え、デバッガを動かして検証できる状態ではないので、推測ですが、要するに制限の計算式ではグローバル変数は使えるようでいて使えない場面(この場合は検索時)があるというのが結論のようです。

では、実際にはどうすればいいのか? 1つは、ターゲットテーブルに対してusernameフィールド同士が等しいというリレーションシップを設定したユーザテーブルをtarget_userとすれば、「username = GET(アカウント名) or target_user::groupname = GET(アカウント名)」という式を設定すればいいかと考えました。ですが、あえて、設定がわかりやすいように、アクセス権セットを2つ作り、通常ユーザ用とグループユーザ用として適用し、前述の式のorで区切った前半を前者、後半を後者の「制限」の式に設定しました。ユーザ作成部分はレイアウトをベースにボタンを作って対処してあるので、作り分けることは完全に自動にできます。1つのアクセス権セットにするのもいいのですが、分けておくほうが、後々、いろんな場面に対応できそうな気がして、アクセス権セットを2つに分ける方を選択しました。

ちなみに、「username = GET(アカウント名) or target_user::groupname = GET(アカウント名)」という式で大丈夫だろうと思われるかもしれませんが、ここでのユーザテーブルの整合性が合わない状況の場合、思いがけない結果になります。{is_group, username, groupname}というスキーマに対して{false, user1, boss}{false, user2, boss}{true, boss, boss}という3レコードがあったとします。これを元に、usernameフィールドにuser1、user2あるいはbossという文字列を入れて、ターゲットテーブルに対してどのユーザのレコードかを記録します。bossでログインをした場合は、前述の式のor以降がtrueになることから、user1やuser2が所有しているレコードに対してもbossは参照できます。しかし、ここで、bossがグループユーザではなくて通常のユーザになったとします。is_groupをfalseにしただけだとしたら、user1、user2→bossがグループという情報が残るので、相変わらずbossはuser1やuser2のレコードを参照できます。つまり、user1やuser2のグループを””にするなどの処置が必要となるわけです。計算式にグループユーザかどうかという分岐を入れれば、この問題は回避できると考えて$$変数を使おうとしたのですが、前述の通り、これはうごかない式になってしまいました。複数のアクセス権セットに分離している場合でも同様なことは発生します。その場合、アクセス権セットを別のものに設定し直しが必要になり、これはスクリプトではできない作業になります。同様に通常のユーザをグループユーザに昇格した場合も同様な懸念がありますが、その場合、他のユーザを新たにグループになったユーザに所属させる作業が発生するので、自然に整合性は取れるかと思われます。アクセス権設定が複数ある場合はその所属変更が必要になります。ということで、アカウント作成後は、is_groupの値を変更しないというのがシンプルな解決策になりそうです。いずれにしても、運用間違えるとセキュリティの問題が発生しそうな案件なので、諸々、注意深く実装する必要はありそうです。

macOSで動くFileMaker Server 2024でPHP

FileMaker ServerにはPHPが付属しなくなって久しいのですが、現状、特にFileMaker Serverで動かなくても不便はなく、PHPの処理が欲しい場合は別途サーバを立てるのが最善策なのはいうまでもありません。しかしながら、「どうしても動かしたい」という声が後を絶たず、ここでとりあえず動かす方法をまとめておきます。そもそも、Clarisからの文書として「FileMaker ServerインストーラへのPHP添付の廃止について」があるのですが、内容が非常に込み入っていて、しかも何をしているのかわかりにくいこともあって、PHPを動かすことに躊躇している人も多いかもしれません。この文書は、FileMaker API for PHPという懐かしいライブラリを利用するための方法まで書いてありますが、今時のPHPのバージョンでこのライブラリが無事に動くとは考えづらく、その方法までは不要なのが一般的ではないかと思われます。

以下の方法が成功したのは、macOS Sequoia上のFileMaker Server 2024の場合です。これは対応OSではないのに成功したということです。また、Sequioa上ではFileMaker Server 2023でも成功しました。これも対応OSではありません(2024版はSequioa対応かどうかは公表されていないですね)。一方、macOS Ventura上のFileMaker Server 2023ではエラーになってうまくいきませんでした。これは対応OSであるのに上手くいかなかった組み合わせです。macOS内蔵のApache2に、外部からインストールするphpのモジュールという組み合わせの相性があるようなので、上手くいかない場合もあるという覚悟で進めましょう。

macOSへのFileMaker Serverのインストールはすでにインストールされているものとします。これにHomebrewをインストールします。インストール方法はサイトに記載されているので、それに応じてインストールしてください。インストール後、以下のようなコマンドを叩いて、phpをインストールします。執筆時点では、8.1以降は@をつけるので良いのですが、7.4はタップというサードパーティによるモジュール提供になっているので、コマンドの入れ方が変わります。コマンドラインでphpのバージョンを切り替えるには、unlink/linkを使います。

//最新版のPHPのインストール(執筆時点では8.3)
brew install php
//特定版のインストール
brew install php@8.1
//PHP 7.4のインストール
brew tap shivammathur/php
brew install shivammathur/php/php@7.4
//バージョンの切り替え
brew unlink php
brew link php@7.4

ちなみに、通常は最新版を使うけど、PHPのモジュールとしては古いものを使うということもアリです。以下、自分の使いたいバージョンのパスを確認してください。原則、/opt/homebrew/Cellerというディレクトリ以下にありますが、状況によって違うので、見つからない場合はいろいろ情報を探ってください。

次にコード署名を行います。コード署名とは、秘密鍵/公開鍵を使った暗号化の仕組みの応用で、ざっくり言うと「誰が作ったコードかわかる仕組み」です。iPhoneのアプリケーションでは当初より署名必須となっており、Appleが発行する証明書を利用して署名されていないと、App Storeへの登録ができなくなっています。この手法はmacOSにもかなり浸透してきており、macOSに付属しているApache2のバイナリhttpdも当初よりコード署名がなされています。

PHPはApache2用のモジュール(拡張子.so)を利用します。このモジュールは、httpdと同じプロセス空間で動きます。イメージ的にはhttpdがphpのモジュールを読み込むと思えば良いでしょう。しかし、macOSの制限により、httpdが署名されている場合は、読み込むモジュールも署名されていないといけません。homebrewで得られるモジュールは署名されていないので、そのままではモジュールとして読み込んで実行ができないのです。そこで、brewで得られたphpのモジュールに署名して、httpdが読み込んで利用できるようにする必要があります。

証明書を作る方法はいろいろあるのですが、「キーチェーンアクセス」アプリケーションを利用して、いかような手順で進めて作成される自己署名証明書で大丈夫です。ポイントは、証明書のタイプを「コード署名」にすることと、指定した名前を忘れないようにすることです。この証明書はキーチェーンに入ります。

キーチェーンにコード署名用の証明書を作ったら、コマンド入力を進めます。まず、/opt/homebrew/Celler以下にあるPHPのパスを探って、phpのモジュールを探します。バージョン等でパスの形式が違う場合がありますが、特定バージョンのphpのフォルダには.soファイルは1つだけなので、それをともかく探せば良いでしょう。そして、codesignコマンドで署名を行います。ちなみに、証明書を作ったユーザでログインしてターミナルで作業する範囲では、codesignコマンドはキーチェーンと同一のデータを使うので、コマンドにキーチェーンの場所を指定する必要はありません。コード署名をしたら、署名を読み出してみて、証明書の名前がAuthorityキーの値に見えることを確認しましょう。

モジュールを特定する(以下、「モジュールのパス」と記載)
/opt/homebrew/Cellar/php/8.3.12_1/lib/httpd/modules/libphp.so
コード署名する
codesign -f --options runtime --timestamp -s "msyk-test" モジュールのパス
//コード署名を表示する',
codesign -d -vvv モジュールのパス
//以下、表示結果Authorityの部分が、証明書の名前(作成時に指定したものとなっている)
Executable=/opt/homebrew/Cellar/php@7.4/7.4.33_6/...
Identifier=libphp7
Format=Mach-O thin (arm64)
   :
Authority=msyk-test

続いて、Apache2の設定ファイルを編集します。設定ファイルは、/Library/FileMaker Server/HTTPServer/conf以下にあります。ファイルはhttpd.confなのですが、以下のように、さらに2.4という拡張子が増えているファイルがあります。これを-iをつけたlsコマンドで見ると、1列目にi-nodeが見えています。i-nodeはファイルシステムでのファイルの中身を特定する番号を思ってください。よく見ると、httpd.confとhttpd.conf.2.4は同じi-node番号です。つまり、1つのファイルの実態にファイル名が2つ割り当てられているという状況で、これらは「ハードリンク」という状態であると呼ばれます。つまり、同じ中身なのです。

% ls -l -i
total 424
158411791 drwxrwxr-x  32 fmserver  fmsadmin   1024 10  9 10:19 extra
158417655 -rwxrwxr-x@  1 fmserver  fmsadmin  18175 10  8 13:32 httpd.conf
158417655 -rwxrwxr-x@  2 fmserver  fmsadmin  18144 10  8 11:57 httpd.conf.2.4
158412007 -rwxrwxr-x   2 fmserver  fmsadmin  13077  1 25  2024 magic
158412007 -rwxrwxr-x   2 fmserver  fmsadmin  13077  1 25  2024 magic.2.4

ここで普通にファイルを変更してもいいのですが、2.4が付いた方がバックアップと売れば、httpd.confは別ファイルとして変更したいような気がするので、たとえば次のようにコマンドを入れて、http.confとhttpd.conf.2.4を別物にしてしまうという手はあります。2つ目でhttpd.confは消えるのですが、中身は別のリンクhttpd.conf.2.4から参照できる状態なので、事実上ファイルは消えません。

cd /Library/FileMaker Server/HTTPServer/conf
rm httpd.conf
cp http.conf.2.4 httpd.conf

ということで、設定ファイル/Library/FileMaker Server/HTTPServer/conf/httpd.confを修正します。以下に、元の状態とその範囲の修正結果を示します。ページ内で、phpというキーワードを探せばすぐに元の範囲は特定できます。その前にLoadModule、そしてFileMatchディレクティブの中にSetHeaderを定義します。モジュールのパスや証明書の名前は、もちろん、前に示したものを指定します。

//元の状態',
<FilesMatch "\.php$">
    Require all denied
</FilesMatch>
//修正した状態',
LoadModule php_module モジュールのパス 証明書の名前
<FilesMatch "\.php$">
    SetHandler application/x-httpd-php
</FilesMatch>

なお、ここで、LoadModuleディレクティブは、通常は証明書の名前は設定できません。Apacheのサイトでもこんな引数はありません。おそらく、macOSに搭載されているhttpdは特別にこのような設定を入れることができるようになっていると思われます。

ちなみに、Apache2のエラーログなどを見ると、正しい状態でもこのようなnoticeメッセージが出てきます。パスが違っていたり、署名されていなかったり、名前が違ったりすと別のエラーメッセージになり、Apache自体が起動しません。

[Tue Oct 08 12:44:50.253541 2024] [so:notice] [pid 17129] AH06662: Allowing module loading process to continue for module at /opt/homebrew/opt/php/lib/httpd/modules/libphp.so because module signature matches authority "Developer ID Application: Nii Masayuki (W3WVRUYJRT)" specified in LoadModule directive

ということで、お決まりの、phpinfo()関数の結果は以下のとおりです。Darwin OS内で動いているのが見えます。もう、HomebrewのPHPは、Sequoia対応になっていますが、この時はSonoma対応の段階でしたけど、ちゃんと動いていました。

そういうわけで、FMDataAPIについても動作確認しました。一連の作業は、自分の普段使いのMacで確認したので、HomebrewによるPHPはいろんなものが最初から入っているため一発で動きましたが(というか、その環境でFMDataAPIを開発している)、足りないモジュールがあればbrew install モジュール名 等でインストールしましょう。

一連の作業で面白い動作を発見しました。codesignコマンドなのですが、SSH経由でログインをしてコード署名を行うと、adhocという名前が出てくる、おそらくはちゃんとしてない署名になります。Authorityは設定されないので、ApacheのPHPモジュールに署名しても稼働はしません。つまり、codesignコマンドは、macOSにログインをして、その上でターミナルから起動して署名しないといけないのです。セキュリティ的な理由なのか、あるいはキーチェーンの扱いなんかがあるのかもしれませんが、ともかく、ログイン方法でコマンドの動作が違って場合によっては正く署名されないというのは落とし穴かもしれません。プロバイダにあるMacや、鍵のかかったサーバルームにあるMacなんかの場合はよく注意をしましょう。

そういうわけで、HomebrewがインストールしたPHPのモジュール、macOSに内蔵されているApache2のhttpdを使うということで、コード署名が必要になるということだったのですが、ちなみに、Homebrewのアップデート(brew update/brew upgradeコマンド)を行うと、場合によってはバイナリが置き換わります。上記のような手順の場合、PHP 8.3の新しいマイナーバージョンの登場で古いものは消されるので、つまりはアップデートがあればその都度コード署名をしなければならないという、面倒かつ、実運用では危険な状態でもある点に注意を払う必要があります。

ちなみに、FileMaker Serverは入れておきたいけど、PHPでも開発作業等をしたい場合、つまり、それぞれ別々に利用するのが一般的な場合は、ここにあるような作業をしなくても、phpを-Sオプションをつけて、サーバモードで動かすのが普通です。公開ディレクトリをカレントディレクトリにして、php -S localhost:9000などとコマンドを入れて、ブラウザから「http://localhost:9000」に接続します。ということで、やっぱりFileMaker ServerのApache2上でPHPを動かす必要性って限られるんですよね。

nginxが動くFileMaker ServerでPHP

もう、だいぶん前ですが、FileMaker ServerのLinux版は、Webサーバとしてnginxを採用しました。また、もう少し前からになるでしょうか、FileMaker Server自体にPHPがバンドルされていない状態になりました。ということで、nginxでPHPダァ〜!ってなるのかと思ったら、全然情報が見つかりません。まあ、先に結論を言えば、PHPを動かすんだったら、別のサーバー立てるのが何かと安心というのがあるかと思います。しかしですね、「なんとか動かないか」と依頼されることが多々あり、手順をまとめてみました。Ubuntu Server 22.04LTSです。sudo権限を持ったmsykというアカウントでログインして作業をしました。

まず、お決まりのUbuntuのアップデート、そしてFileMaker Serverのアップデートです。Linux版は単にinstallすればアップデートもできて結構便利ですね。FileMaker Serverのインストール方法は、Clarisのドキュメントあるいはサイトをご覧ください。この記事は、FileMaker Serverをインストールした後のお話です。

sudo apt update -y
sudo apt upgrade -y

sudo apt install ./filemaker-server-21.0.2.202-arm64.deb

Apache2は、PHPのモジュール、つまりApache2のプロセスに入り込んで動くバイナリを使っていました。一方で、nginxは、別プロセスで動くPHPに対して、ソケットや通信を使って動きます。ということで、単独のプロセスで動くPHPといえば、PHP-FPM(FastCGI Process Manager)ですね。こちらをセットアップして動くようにする必要があります。モジュール名さえ分かっていればOKですね。以下の1行目で、phpとphp-fpmをインストールしていますが、composerも必要でしょうから一緒に入れておきます。2行目は、systemctlで指定するサービス名は「php8.1-fpm」になります。2行目はphp-fpmを再起動後も起動するように設定しておきます。なお、手元ではApache2も動いてしまっていたので、止めて再起動後にも起動しないようにしておきました。

sudo apt -y install php php-fpm composer
sudo systemctl enable php8.1-fpm
sudo systemctl stop apache2
sudo systemctl disable apache2

続いて、nginxの設定ファイルを変更します。FileMaker Serverは、/opt/FileMaker/FileMaker Server以下にファイルを置きます。スペース入るの嫌ですが、仕方ないですね。その中のNginxServerのさらに下にconfがあり、4つの設定ファイルがあります。fms_nginx.confファイルを修正します。例えば、こんな感じでnanoで編集します。

cd /opt/FileMaker/FileMaker\ Server/NginxServer/conf
sudo nano fms_nginx.conf

修正箇所を以下に示しますが、ファイルのほとんど末尾です。ファイル全体は、大きく分けて、ポート80に対する設定と、443に対する設定があり、後者の { } 内に以下のコードの赤字部分を追加します。最後にコメントになっている箇所があるので、その直前でいいかと思います。このままコピペして大丈夫だと思いますが、一応、設定のポイントを次に説明します。

    location ^~ /fmi/ {
      proxy_set_header X-Forwarded-Proto https;   # MWPE need ...
           :
      proxy_cookie_path /fmi "/; Secure; HttpOnly; Max-Age=43200";
    }

    location ~ \.php$ {
        fastcgi_pass  unix:/var/run/php/php-fpm.sock;
        fastcgi_index index.php;
        include       /etc/nginx/fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

#   location / {
     # First attempt to serve request as file, then
     # as directory, then fall back to displaying a 404.
         :

まず、locationは正規表現を伴っていますが、これは、拡張子が.phpのファイルに対するリクエストが来たらこの後の定義に従うということです。fastcgi_passは、ソケットあるいはURLを指定しますが、ここではソケットを指定しています。php-fpmはソケットあるいはTCP/IPのポートが開きますが、ソケットについてはこの後のphp-fpmの設定ファイルwww.confの中に記載されているので、それと合わせる必要があります。

fastcgi_indexは、ディレクトリへのリクエストの場合、index.phpファイルを探して、そのファイルへのリクエストとして処理するという意味です。

includeの後に、ファイルのパスを指定しますが、ここで指定したファイルはFileMaker以下ではなく、/etc/nginx以下、つまり一般的なnginx側のファイルです。よほど、複製して使う方がいいかと思いましたが、書き換えないので、標準nginxのファイルをそのまま使います。このファイルや、次のfastcgi_paramでは、CGI呼び出しのパラメータを指定します。PHP的にいえば、$_SERVER変数で得られる値を定義します。もちろん、キーと値を指定します。fastcgi_paramsファイルには一般的なCGIのパラメータがほぼ記載されているのですが、$_SERVER[‘SCRIPT_FILENAME’]の値については、モジュールで動かすPHPと若干違っているので、その設定がファイルに加わっています。変数$fastcgi_script_nameは、例えば、「/info.php」のような、リクエストで指定したパスです。変数$document_rootは、nginxのドキュメントルートで、root “…”で指定すると勝手に変数が定義されます。これらの変数をつなげることで、つまりは、稼働しているスクリプトへの絶対パスがパラメータとして伝達されるということです。

続いて、php-fpmの設定を行います。モジュール版の場合、Ubuntsuではwww-dataという名前およびグループのプロセスとしてApache2が稼働しているので、それらのユーザが前提となります。一方、FileMaker Serverは、nginxも、fmsuser/fmsadminというユーザ・グループで稼働します。それに合わせるため、設定を変更します。例えば、次のようなコマンドで編集作業を行います。

cd /etc/php/8.1/fpm/pool.d/
sudo nano www.conf

修正箇所は2箇所4行ですが、他の部分を入れ混ぜるとちょっと冗長なので、修正部分だけを以下に抜き出します。つまり、user, group, listen-*をそれぞれFileMaker Server向けに書き換えます。

    :
user = fmserver
group = fmsadmin
    :
listen.owner = fmserver
listen.group = fmsadmin
    :

設定はこれだけです。ということで、プロセスを再起動して…と言いたいのですが、FileMaker ServerはApache2だけの頃からも再起動が怪しかったですが、nginxでもやっぱりダメです。こちらのブログ「FileMakerServer での nginx チューニングをするための前準備」で細かく検討されていますが、ここではサーバを再起動させることにします。

再起動後、ドキュメントのルート、/opt/FileMaker/FileMaker\ Server/NginxServer/htdocs/httpsRootに、ファイル名info.php、中身は「<?php phpinfo();」というお決まりのテストファイルを作成します。はい、無事に出ました。

なお、ドキュメントルートは、例えば以下のコマンドで、自分のアカウントに書き込み権限があるようにしておくと良いでしょう。実行については読み出し権限がグループのfmsadminに設定されているので問題ありません。

cd /opt/FileMaker/FileMaker\ Server/NginxServer/
sudo chown -R msyk htdocs
cd htdocs/httpsRoot
echo "<?php phpinfo();" > info.php

ちなみに、FileMaker Serverでnginxをセットアップしているので、NginxServer/logsにログがあるのではと思うところで、実際にアクセスログなどはあります。ですが、設定ファイルのエラー等は、/var/log/nginx/error.logに書かれます。設定ファイルの間違い探しでは、こちらのファイルを参照しなければなりません。なお、PHPはそのままにしているので、/var/log/php8.1-fpm.logに書かれます。iniファイルは、/etc/php/8.1/fpm/php.iniですので、必要ならそのファイルを修正しましょう。

さて、FileMakerでPHPとなると、普通はFMDataAPIクラスを使うと思いますが、FMDataAPIも順調にバージョンアップして、Ver.32になっています。なお、Ver.32以降はPHP 8.1が最低条件ですので、PHP 7の方は前のバージョンを使ってください。Ubuntu 22はかろうじてPHP 8.1ですので最新版を使えます。例えば、次のようにコマンドを入れて、サンプルを動かしてみましょう。サンプルのデータベースは、FMDataAPIのページを参考にして、FileMaker Serverにセットアップしておいてください。

cd /opt/FileMaker/FileMaker\ Server/NginxServer/htdocs/httpsRoot
git clone https://github.com/msyk/FMDataAPI
cd FMDataAPI
composer update

これで必要なライブラリがセットアップされる…と思ったら、Problemとして2つ項目が出ます。

Composer is operating significantly slower than normal because you do not have the PHP curl extension enabled.
Loading composer repositories with package information
Updating dependencies                                 
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Root composer.json requires PHP extension ext-curl * but it is missing from your system. Install or enable PHP\'s curl extension.
  Problem 2
    - phpunit/phpunit[3.7.0, ..., 3.7.38, 4.0.0, ..., 4.8.36, 5.0.0, ..., 5.2.7, 8.5.12, ..., 8.5.40, 9.3.0, ..., 9.6.21, 10.0.0, ..., 10.5.36] require ext-dom * -> it is missing from your system. Install or enable PHP\'s dom extension.
    - phpunit/phpunit 4.8.20 requires php ~5.3.3|~5.4|~5.5|~5.6 -> your php version (8.1.2) does not satisfy that requirement.
    - phpunit/phpunit[5.2.8, ..., 5.7.27] require php ^5.6 || ^7.0 -> your php version (8.1.2) does not satisfy that requirement.

色々書いてありますが、要するにcurlとdomの機能が入っていません。これらは、モジュール版と同様に、aptコマンドでインストーすることができます。インストール後、念のためphp-fpmを再起動します。前の続きなら、途中のcdコマンドは不要ですが、カレントディレクトリを明示するために書いておきます。

sudo apt install -y php-curl php-dom
sudo systemctl restart php8.1-fpm
cd /opt/FileMaker/FileMaker\ Server/NginxServer/htdocs/httpsRoot/FMDataAPI
composer update

これで、ブラウザからサンプルコードの.phpファイル(例えば、https://localhost/FMDataAPI/samples/FMDataAPI_Sample.php)を開くと、個別の通信が示されて、途中にはデータベースからデータを取り出す部分などが参照できます。

ちなみに、Ubuntsu上での動いているプロセスを見てみます。例えば、以下のようなコマンドで見てください。綺麗にレイアウトされないので、ここには結果は出しません。

ps aux

ここでは、プロセス名php-fpmで稼働ののち、FileMaker Serverがnginxプロセスを起動して、その後にFileMaker Serverのさまざまなプロセスが稼働しているのがよ句わかります。php-fpmはfmsuserがプロセスのオーナーになっていますし、nginxも同様です。

ということで、FileMaker Server 2024 on Ubuntsu 22において、PHP 8.1が起動しました。ここにはいちいち書きませんが、INTER-Mediatorも動いてサンプルが稼働しています(MySQLでテストしましたが〜笑)。

Webページでのテキストフィールド移動

同じことで何度も悩んでしまったので、ちゃんとまとめて書いておこう。タイトルだけでは何の意味かわからないですが、具体的には、ログインパネルのユーザ名とパスワードと思ってください。通常、ユーザ名を入力してEnterキーを押すと、パスワードのフィールドに自動的に移動したいですよね。ということで、ユーザ名のテキストフィールドをuserBox、パスワードのテキストフィールドをpasswordBoxで参照しているとして、まあ、例えば、こんなプログラムを組みます。イベント設定はいろんな方法があるのでそこは置いといて、keydownイベントが来て、押されたキーがEnterあるいはNumpadEnterなら、パスワードボックスに飛びます。

userBox.onkeydown = function (event) {
  if (event.code === 'Enter' || event.code === 'NumpadEnter') {
    passwordBox.focus()
  }
}

しかし、これだと、Macではおかしな動作が発生します。かな漢字変換途中の文字列でEnterを押すと、確定した文字列がユーザ名だけでなく、パスワード側にも入るのです。つまり、Enterによる確定でユーザ名に確定した文字列が入るのですが、同時に変換途中の状態でパスワード側にフォーカスが移動して、そこでもやっぱり確定されて、両方のフィールドに文字が入ってしまうようなのです。Windowsでは、どうなっているのかというのはちょっと説明しづらいですが、あまりいい感じではありません。

そこで、いろいろ調べると、EventクラスにisComposingというプロパティがあって、変換途中ならtrueになることがわかりました。ということで、結論としてはこのようにisComposingを考慮すればいいということになります。つまり、Enterキーを押しても、変換途中ならばパスワードフィールドへの自動移動をしないということです。

userBox.onkeydown = function (event) {
  if ((event.code === 'Enter' || event.code === 'NumpadEnter')
      && !event.isComposing) {
    passwordBox.focus()
  }
}

ちなみに、caniuse.comによると、isComposingはモダンなブラウザならどれもサポートしているので安心して使えます。

KeycloakをIdpに使ってSAML認証(3)

この記事は、KeycloakをIdpに使ってSAML認証(1)およびKeycloakをIdpに使ってSAML認証(2)に続く記事である。(1)はKeyCloakのインストール、(2)はKeyCloakをSAMLのIdPとして、INTER-Mediatorで作ったアプリケーションに認証処理を提供する方法を記述したが、認証が通ったことはわかるものの、SAML属性にユーザ名が入っていないということで、INTER-Mediator側では想定した状態になっていなかった。そして、その点は解決したので、ここに記載しておく。

KeyCloak をIdPとし、SimpleSAMLphpをSPに設定している場合で、SP側の管理ページに入り、そこで認証をしてみる。そこでは、アプリケーション側で得られてるSAMLの属性がわかるので、その部分の確認と、ユーザ名を入れる拡張を進めてみた。まず、単にKeyCloakのClientとしてSAML SPを登録した状態では次のようになる。管理ぺ時のTestのところにSPが見えている(ここでは「default-sp」)。そのSPをクリックする。

すると、KeyCloakの黒い背景の認証ページに移動する。ここで、既存のkcuser1で認証をした場合、次のように、得られる属性が見えている。(2)で説明した通り、ここでも同様にRoleという属性しか見えていない。

しかしながら、以下のようにKeyCloak側を設定すると、次の図のように、kcuser1でログインした結果、そのユーザの姓名とメールアドレスに加えて、ユーザ名も取得することができた。

INTER-Mediator側で、取得した属性をチェックすると、このような感じになる。以前は、Roleだけだったのが、ユーザ名や氏名、メールアドレスも、ともかく取得できている。

ここで、INTER-Mediatorのparams.phpファイルでは、このように取得される属性から必要なデータを得られるように、$samlAttrRules変数の配列を定義しておく必要がある。この配列のキーは、ユーザテーブル(authuser)のフィールド名である。配列のキーに対する値は、SAMLの属性から取り出す時のキーと、|で挟んで配列の要素番号を指定している。メールや氏名については、前述の属性のダンプにあるように、複雑な記号がキーになっているので、それをコピペするなどして、きちんと配列を定義しなければならない。

いずれにしても、これで、INTER-Mediatorのアプリケーションで、KeyCloakをIdPとして認証ができることが確認できた。

どうすれば属性を増やすことができるのか

ということで、タイトル通りの疑問がここで沸く。言い換えれば、KeyCloakに設定したユーザでの認証については、「何かしなければRoleしか得られない」ということだ。

まず、左側で、Client Scopesを見てみよう。ここをみると、SAMLプロトコルに対しては、role_listという名前のものしか定義されておらず、他はみんなOpenIDのものである。以下の画像には1ページ目しか見えていないが、もう1項目だけ次のページにあり、それはOpenID向けのものである。ここを見て、「属性にRoleしかない」という状況と、Client scopesにSAMLのものはrole_listしかないという結果に大きな関連性があると着目して、ここに設定を加えれば良いということがわかる。

このリストで、Create client scopeボタンをクリックする。そうすると、以下のような画面になる。ここで、Nameを指定するが、この名前は、このリスト上の名前であり、すでに存在する名前はつけられない。ここでは、snとした。そして、もう1つProtocolからSAMを選択しておく。とりあえず、この2つで良いようだ。Protocolは後から変更できないので忘れないようにする。そしてSaveボタンをクリックする。

Saveボタンをクリックすると、Clinet scopeの1項目を編集する画面に入るのだが、上部のタブのMappersをクリックする。最初は次の図のように、No mappersとなっている。ある属性を得るには、マッパーを利用して、既存のどこかの値を持ってくるという設定を記述しないといけないようである。

ここでまず、Add predefined mapperボタンをクリックして、つまりは既定義のマッパーを探して設定する。ちなみに、既定義のマッパーが一覧されるので、該当する項目にチェックを入れて、Addボタンをクリックする。ただ、これをよくみると、既定義のマッパーでは、sureName、givenName、emailの3つしかない。ここでは、snに対するものとして、X500 surenameにチェックを入れて、Addをセットする。ちなみに、すでに設定したものはこのリストでは表示されなくなる。

この設定を、sn、gn、mailというClient scopeに対して行った。

では、usernameはどうすればいいか?まずは、usernameに対応するためのClient scopeの1項目を作る。もちろん、ProtocolをSAMLにするのを忘れないようにする。そしてマッパーを登録すが、usernameのマッパーは既定義には存在しないのでConfigure a new mapperをクリックする。すると、次のような選択画面が表示される。何を選択するか迷うが、とりあえず、User Attribute、User Propertyのいずれも成功している。ここではUser Attributeを選択したとする。

すると、次のようなマッパーを定義する画面に入るので、ここで、Name以下4つの項目で全部、usernameと入力する。なお、ここでは最後の項目にあるように、SAML属性上でのキー名も入力できるので、属性上の値の解釈も分かりやすくなる。入力後、Saveボタンをクリックして保存しておく。

こうして、4つのClient scopeを追加した。Client scopeの一覧で、SAML Protocolに絞って一覧を示すとこのような感じである。username以外は、既定義のマッパーを使っている。

これでOKかと思ったのだが、まだRoleしか属性に入っていない。さらに続いて、次のように設定を行う。Clientsを選択すると、クライアントのリストが表示される。ここで、追加したSAML SPの項目のリンクをクリックする。

そして、Clientの設定画面が出てくるので、上部のタブで、Client scopesを選択する。規定値では、role_listが見えているはずだが、いずれにしても、Client scopesに登録し、さらにここで追加する必要があるということで、ここで、さらに選択を加えないといけない。

Add client scopeボタンをクリックすると、次のように、Client scopesで定義したProtocolがSAMLの項目が一覧される。とりあえず、ここでは全ての項目にチェックを入れて、Addボタンをクリックする。さらに選択するポップアップが出てくるので、Defaultを選択しておく。

SAML SPのClient scopesの設定が次のようになったらOKである。

ここまで設定を行うことで、最初に示したように、SAML属性にusernameや氏名、メールアドレスを含めることができ、INTER-Mediatorでの利用が可能になった。

KeycloakをIdpに使ってSAML認証(2)

この記事は、KeycloakをIdpに使ってSAML認証(1)に続く記事である。(1)では、ともかくKeyCloakを稼働するところまで進めた。そして、KeyCloakをIdPとし、SimpleSAMLphpを使ったINTER-Mediatorのアプリケーションまで稼働する。なお、SP側は、SimpleSAMLphp Ver.2を使ってみる(3)の設定が終わっているものとする。SPは、IdPもSPもSimpleSAMLphpを利用していて、default-spがとりあえず、SimpleSAMLphpのIdPに接続されている。

KeyCloak側にRealmを1つ定義する

ここでまず、KeyCloak側にRealmを1つ定義する。KeyCloakの管理画面にadminユーザでログインをして、左側のナビゲーションバーの一番上にあるドロップダウンリストにある「Create realm」ボタンをクリックする。

すると、次のようなフォームが表示される。ここではとりあえず、Realm Nameだけを入力して、Createボタンをクリックすれば良い。以下は、IM_Testと入力をした。

左側の上の方に、作ったRealmのIM_Testが見えているはずだが、ここでRealmを切り替えて利用できる。以後はここで作成したIM_Testが選択された状態で作業を進めることにする。

KeyCloak側にユーザを定義する

続いてユーザを定義する。左側でUsersを選択すると、ユーザの一覧が表示されるが、その中にあるAdd Userボタンをクリックする。すると、以下のようなフォームになるので、とりあえず、Usernameだけは入力しておく。ここではkcuser1とした。Emailも入力されている。そして、Createボタンをクリックすると、ユーザは作られた。

次に作ったユーザにパスワードを与える。管理画面の左側でUsersを選択すると、作ったユーザkcuser1が見えているはずだ。そのユーザ名のリンクをクリックすると。そのユーザの設定変更ができるフォームが見えている。ここで、右側上部のタブにあるCredentialsをクリックして、パスワードを設定する。

Credentialsをクリックすると、次のようなパネルが表示される。ここで、PasswordとPassword Confirmationに同じパスワードをキータイプする。TemporaryはOnだとログイン時にパスワードの変更が必要になる。ここではテストなので、Offにしておくのが良いだろう。そして、Savaボタンで、パスワードが設定される。

IdP(KeyCloak)のメタデータをSPに登録

ここからは、IdPとSPのメタデータの交換である。まずは、IdPのメタデータをSPに登録する。別記事の手順で作られたSPだが、SP側の設定はほぼそのままにして、IdPのメタデータだけを入れ替えることにする。

IdPのメタデータは、左側のReaml settingsをクリックして、Generalのタブの一番下にあるEndpointsというところにある「SAML 2.0 Identity Provider Metadata」というリンクをクリックする。

すると、次のように新たなウインドウにIdPのメタデータが表示される。このXMLを利用するのだが、ブラウザでXMLのコピペをするとしても、ブラウザが余計なことをしてしまうことになりそうなので、ここでは、URLをコピーしておく。そして、作業用のコンピュータ側でいいので、「curl -O メタデータのURL」とコマンドを入れて確実に中身の変更が行われない状態で、ファイルに納める。descriptorというファイルが作られるが、それをエディタで開いて、中身にコピーしておくと良い。

このメタデータをSP側に登録するのだが、SPはSimpelSAMLphpであるので、メタデータはXML形式ではなくPHPのプログラムにしなければならない。そこで、SP側の管理ページを開く。URLを示す意味があるかは若干疑問だが、別記事の通りにセットアップしてれば、「https://demo.inter-mediator.com/saml-trial/lib/src/INTER-Mediator/vendor/simplesamlphp/simplesamlphp/public/admin」で管理ページへのログインができる。ログイン後、上部で「連携」を選択する。ここにある「ツール」の「XMLをSimleSAMLphpメタデータに変換」の部分をクリックする。

すると、メタデータパーサと書かれたページが出てくるので、XMLメタデータ部分にペーストする。つまり、IdPのメタデータそのものを、ここに貼り付けて、「パース」ボタンをクリックする。

すると、次のように、PHPのコードに変換されたものが「変換されたメタデータ」の部分に見えるようになる。このPHPのコードを慎重にコピーして取り込んでおく。

SPの設定を変更する。設定ファイルのsaml20-idp-remote.phpを修正する。テキストエディタで開いて、末尾に変換されたIdPのメタデータをペーストすれば良い。従来のIdPのメタデータはコメントにしておくのが良いだろう。

SPのメタデータをIdP(KeyCloak)に登録

次は、SPのメタデータをIdPに登録する。SPのメタデータは、SPの管理画面で「連携」のところのdefault-spの下にあるVの部分をクリックすると、表示される。XMLとPHPの両方で表示されるが、ここで必要なのはXMLの方である。ここではコピペは必要なく、SAML Metadataの直下「You can get the metadata XML on a dedicated URL:」のすぐ下にある1行のURLの部分の右端にクリック可能なボックスが出てくる。これをクリックすると、メタデータのXMLがファイルとしてダウンロードされる。ファイル名は、default-sp.xmlという名前になる。

KeyCloakの管理画面に戻る。IM_TestのRealmが選択されていることを確認した上で、左側でClientsをクリックする。以下のような画面になるので、リストの上にあるImport clientの部分をクリックする。

インポートの画面に遷移するが、ここでは、Browseボタンをクリックして、先ほどダウンロードしたSPのメタデータファイル(default-sp.xml)を選択する。すると、Resource fileの部分にメタデータファイルの中身が展開される。ここまででSaveボタンを押して保存をしておく。

アプリケーションを動かしてみるが…

では、実際にSAMLに対応したアプリケーションを動かしてみる。認証が必要な場面で、KeyCloakの認証画面に移動し、登録したユーザkcuser1での認証は通った。なお、名前を入力していなかったので、このように入力を促すパネルが出るが、認証自体は通っているようである。

しかしながら、その後には、次のようなメッセージが出てきた。というか、これはINTER-Mediatorが出すメッセージである。INTER-Mediatorは、認証時に得られる属性の配列から、ユーザ名や氏名、メールアドレス等を取り出すようにしており、ユーザ名の取得は必須にしてある。$samlAttrRules変数は設定ファイルに記述する属性の何を取り出すのかを指定する配列であるが、ここでは既定の状態でユーザ名の取得が正しく取れていないことを意味する。

SAMLのやり取りをみるとRoleしか来てない

デバッグメッセージを表示してもう少し詳しく見てみよう。右側、中央あたりに見えている6要素の配列が、IdPから得られたデータである。これをよくみると、Roleというキーがあるので、Roleが得られていることはわかるのだが、それ以外のデータが全くない。つまり、ログインは成功したもののIdPからRole以外のデータが全く来ていないということになり、その結果、どのユーザでログインしたのかは、アプリケーション側は全く検知できない状態になってしまっているということである。

つまり、KeyCloakを既定の状態で使うままではどうやらINTER-Mediatorでの認証はできない。ユーザ名を「気にしない」アプリケーションなら良いのかもしれないが、ユーザ名を取得できることを必須としているINTER-Mediatorでは対策が必要である。

ちなみに、SAML-tracerでチェックしてみた。ユーザ名とパスワードを入力した直後のSAMLのやり取り行をみてみると、SAML2.0 Responseはあるのだが、ここでやり取りされるデータは非常に少ない。

ちなみに、INTER-Mediatorを使って稼働している別のシステムではこのようなデータが見えている。なお、IdP, SPともにSimpleSAMLphpを使い、IdPはADに対してLDAPで接続して、ADユーザでのログインを可能にしている。ログイン直後に、SAML2.0 AttirubuteStatementの部分には、たくさんのデータが見えているが、事実上、これはADのユーザデータである。見てすぐにわかるように、cnでユーザ名、snとgivenNameで氏名が得られるので、そのようにINTER-Mediatorをセットアップして利用している。

ということで、KeyCloakとSimpleSAMLphpのSPは、接続等のセットアップはできるが、そのままではRoleしか得られないので、INTER-Mediatorでは認証はおそらくできているとしてもユーザ名が得られないので、認証していないとみなしているというところで、その解決策がわかれば、続きの記事を書くとする。