kintone 2024/07 アップデートで追加されたプラグインAPIに気をつける

## 前置き

[kintone 2024 年 7 月アップデード](https://kintone.cybozu.co.jp/update/main/2024-07.html) で、かねてから [API ラボ](https://cybozu.dev/ja/kintone/docs/api-lab/) で開発中となっていた [プラグイン関連 API](https://cybozu.dev/ja/kintone/docs/api-lab/rest-api/plugins/)が正式リリースとなりました。
ただしこの API は使用方法を誤るとそこそこ致命的な事態にもなりかねないため、その勘所についてお話しようと思います。

## プラグイン API でできること

まず今回新たに追加された API をざっと見ていきます。

1. インストール済みのプラグインの一覧を取得する
2. インストールが必要なプラグインの一覧を取得する
3. プラグインを追加しているアプリの一覧を取得する
4. プラグインを読み込む
5. プラグインを更新する
6. プラグインをアンインストールする

使い方云々はとりあえずおいておいて、それぞれの API の内容や使いどころについて軽く触れてみましょう。

### 1. インストール済みのプラグインの一覧を取得する

https://cybozu.dev/ja/kintone/docs/rest-api/plugins/get-plugins/

kintone 環境にインストール済みのプラグインの情報をまとめて取得する API です。
以下のようなフォーマットで返却されます。

```json
{
  "plugins": [
    {
      "id": "プラグイン固有のID",
      "name": "プラグイン名",
      "isMarketPlugin": "プラグインストアのプラグインかどうか",
      "version": "プラグインのバージョン"
    }
  ]
}
```

`name` や `version` はプラグインが内包する `manifest.json` から読み取られるものと思います。（ロケールとかどうなっているかは確認していません）
であれば `description` とか `homepage_url` の値も引いてきてくれても良いんじゃないかなって気はします。

**この API は誰でも実行可能です。**
（※ブラウザ環境なら kintone にログインしているユーザーなら誰でも、Node.js 環境なら適切な auth を設定していれば誰でも、と言う意味です）

### 2. インストールが必要なプラグインの一覧を取得する

https://cybozu.dev/ja/kintone/docs/rest-api/plugins/get-required-plugins/

**インストールが必要** とはどう言う状態かと言うと、**環境内のいずれかのアプリにプラグインが割り当てられているがその環境には当該プラグインがインストールされていない** と言う状態になります。
（それ以外にもあるかも知れないが）

- プラグインといっしょに配布されているアプリテンプレートをインストールしてそこからアプリは作ったがプラグインのインストールをまだしていない
- 別環境で使っていたアプリをアプリテンプレート化して違う環境にインストールした
- プラグインをインストールしており環境内のアプリで利用していたが、プラグインを削除してしまった

などと言った状況が考えられそうです。

返却されるデータのフォーマットは上記の API と同じものですが、そのプラグインがどのアプリにも適用されていない状態だと `name` は `null` になってしまいます。
IP アドレスからホスト名を引ける DNS のようにプラグイン ID からプラグイン名を名前解決できる仕組みがあるわけではないので、これは当然です。
したがってこの結果だけを以てどのプラグインをインストールすれば良いかを判断するのは難しいでしょう。
（いずれかのアプリに追加済みで、プラグインだけアンインストールした状態だと `name` も然るべき値が格納されます）

またこの API のポイントとして、「どのアプリでどのプラグインが」と言う切り口ではない、**環境全体としてインストールが必要なプラグインを列挙する** と言う挙動であると言う点は理解しておいたほうが良さそうです。
アプリのカスタマイズ内で実行したとしても、 **そのアプリに必要なプラグインだけが絞り込まれるわけではない** と言う事です。

なお、アプリ設定内のプラグイン設定でプラグインを **「無効」** にしていても取得できる結果には影響しませんでした。
**有効だろうが無効だろうがプラグインに割り当てがあればインストールが必要として検出される** と言う事です。

**この API は誰でも実行可能です。**

### 3. プラグインを追加しているアプリの一覧を取得する

https://cybozu.dev/ja/kintone/docs/rest-api/plugins/get-plugin-apps/

特定のプラグインがどのアプリに割り当てられているかを列挙する API です。
以下のようにプラグイン ID を指定する必要があります。

```json
{ "id": "プラグイン固有のID" }
```

返却されるのは以下のようなデータです。

```json
{
  "apps": [
    {
      "id": "アプリID",
      "name": "アプリ名"
    }
  ]
}
```

kintone システム管理の **プラグイン** ページですべてのプラグインについてどのアプリに割り当てられているかを一覧で表示できますが、それを単一のプラグインを指定してコードで得られるようになった、と考えると解り易いかも知れません。
とは言え実運用上でどのような場面で使うかな・・・と考えるとあまり使い所は思いつかない気がします。

**この API は cybozu.com 共通管理者のみ実行可能です。（kintone システム管理者でも実行できません）**

### 4. プラグインを読み込む

https://cybozu.dev/ja/kintone/docs/rest-api/plugins/add-plugin/

[ファイルをアップロードする API](https://cybozu.dev/ja/kintone/docs/rest-api/files/upload-file/)でアップロードしたプラグインファイルを kintone 環境にインストールする API です。

この API でアップロード（インストール）したプラグインは [アプリにプラグインを追加する](https://cybozu.dev/ja/kintone/docs/rest-api/apps/settings/add-app-plugins/) API を利用する事でアプリに追加できます。

当該プラグインがインストール済みの環境で実行するとエラーが発生します。

**この API は kintone システム管理者が実行できます。**

### 5. プラグインを更新する

https://cybozu.dev/ja/kintone/docs/rest-api/plugins/update-plugin/

インストール済みのプラグインを更新するための API です。

```json
{
  "id": "プラグインID",
  "fileKey": "ファイルアップロード時に払い出されたファイルキー"
}
```

を渡して実行する形ですが、当該プラグインがインストールされていないとエラーになります。
この API だけでプラグインをインストールする事はできないと言う事です。
また、言うまでもありませんが、更新するプラグインのバージョンがインストール済みのバージョンより新しくなければダメなどの制約はなく、古いバージョンに更新したり同一のバージョンで再度更新したりなども可能です。
（内部的に特にバージョンを検証している挙動ではないと言う事）

**この API は kintone システム管理者が実行できます。**

### 6. プラグインをアンインストールする

https://cybozu.dev/ja/kintone/docs/rest-api/plugins/delete-plugin/

インストール済みのプラグインを削除する API です。
アプリに適用済みの状態であっても関係なくアンインストールされます。

**この API は kintone システム管理者が実行できます。**

## 組み合せた使い方を考えてみる

このように一通り API が揃ったプラグイン API。
どのような利用シーンが想定されるでしょうか。

例えば、kintone とは別の外部サービスとしてインストールしたプラグインを管理するマネジメントコンソール（マネコン）のようなサービスを考えてみます。

- 複数の kintone 環境を束ねて管理することができる
- （ライセンスをクリアしている前提で）マネコンから管理下の任意の kintone 環境にプラグインをインストール・アンインストールできる
- プラグインが適用されているアプリ数などをダッシュボードで表示する

みたいなサービスを作れそうな気がしますね。
（ビジネス的に売れるかどうかはともかくとして・・・）

また、プラグイン自身に自動アップデートの機構を組み込む事もできそうです。

- プラグイン設定画面表示時、（何らかの手段で）プラグインの最新リリースバージョンと kintone へのインストール済みのバージョンを比較する
- 最新のバージョンがインストールされている場合、ダウンロードサーバから zip ファイルをフェッチしてきて、ファイル API で kintone にアップロード、プラグイン API で更新する

連携サービスと異なりプラグインは利用者の方にアップデートをインストールしていただかないと最新バージョンをご利用いただけない構造でしたが、この API を利用すればある程度コントロールができるのではないでしょうか。

## 実際に API を試してみる

それでは kintone が [サンプルプラグイン](https://cybozu.dev/ja/kintone/tips/development/plugins/sample-plugin/) として配布しているものを利用してこれらの API の挙動を確かめてみましょう。

**新しい試用環境を作成して**、上記ページより「いいねプラグイン」などいくつかのプラグインをダウンロードしてインストールしておきます。
![resized-01-001_plugins.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162138/dd9459ef-b804-1b20-aa17-99495eaf94d7.png)

実験するアプリは何でも良いですが、手軽にやるために **顧客リスト** アプリを作成します。
最初から作られているものを使ってもいいです。
![resized-01-002_add-app.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162138/18452239-a15d-1d42-0a58-7b5dc70d3e12.png)

作ったアプリに、インストールしたプラグインを適用しておきます。
必須項目の設定云々は今回の検証には無関係なので放っておきます。
![resized-01-003_added-plugins.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162138/9e7a939b-8fec-29c7-98f4-7854e48e4d9a.png)

アプリのカスタマイズで、以下のようなカスタマイズを追加してみます。
今回追加した JSEdit 上でやると楽ちんです。

```javascript:main.js
(() => {
  "use strict";
  kintone.events.on("app.record.index.show", async (event) => {
    // 1. インストール済みのプラグインの一覧を取得する
    console.log("インストール済みのプラグインの一覧を取得する");
    const plugins = await kintone.api(
      kintone.api.url("/k/v1/plugins.json"),
      "GET",
      {}
    );
    console.dir(plugins);
  });
})();
```

これでレコード一覧画面を表示するとインストールしたプラグインの一覧がコンソールに出力されます。

<details><summary>`GET /k/v1/plugins.json` 出力結果</summary>

```json
{
  "plugins": [
    {
      "id": "lckkahmekfcjioncompafcpdohdgmbam",
      "name": "入力値チェックプラグイン",
      "isMarketPlugin": false,
      "version": "2.3.3"
    },
    {
      "id": "noaekdhemoaagialmfhefckdebaddbpe",
      "name": "自動採番プラグイン",
      "isMarketPlugin": false,
      "version": "2.4.4"
    },
    {
      "id": "ddkejoiejjgpogglbppekbefobhkddig",
      "name": "kintone spreadsheet editor",
      "isMarketPlugin": false,
      "version": "1.0.6"
    },
    {
      "id": "jjkaklfefkfhpglmhjalgojlgbaecnhk",
      "name": "いいねプラグイン",
      "isMarketPlugin": false,
      "version": "1.7.2"
    },
    {
      "id": "fdeplpmkengkldpdlaiegpokgmaabkkb",
      "name": "JSEdit for kintone",
      "isMarketPlugin": false,
      "version": "4.5.3"
    }
  ]
}
```

</details>

次に、インストール済みのプラグインのいくつかを削除してみます。
「いいねプラグイン」（ID: `jjkaklfefkfhpglmhjalgojlgbaecnhk`） を削除してみましょう。

```javascript:main.js
(() => {
  "use strict";
  kintone.events.on("app.record.index.show", async (event) => {
    // // 1. インストール済みのプラグインの一覧を取得する
    // console.log("インストール済みのプラグインの一覧を取得する");
    // const plugins = await kintone.api(
    //   kintone.api.url("/k/v1/plugins.json"),
    //   "GET",
    //   {}
    // );
    // console.dir(plugins);

    // 6. プラグインをアンインストールする
    console.log("プラグインをアンインストールする");
    await kintone.api(
      kintone.api.url("/k/v1/plugin.json"),
      "DELETE",
      {
        id: "jjkaklfefkfhpglmhjalgojlgbaecnhk",
      }
    );
  });
})();
```

これでレコード一覧を表示してみます。

その後アプリ設定のプラグイン画面を見ると、確かに「いいねプラグイン」が削除されているようです。
![resized-01-004_plugin-deleted.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162138/281d8578-55b9-6851-4da8-7d06cda88f77.png)

プラグイン ID が判ればプラグインを削除するのがとても簡単だと言う事がお解りになったかと思います。

## 危険を冒してみる

:::note alert
はじめにお断りしておきますが、ここで紹介するものは非常に危険な内容です。
お試しになる場合は **新規に試用環境を用意する** などしてください。
**決して普段お使いの kintone 環境で実行しないように！！！！**
:::

引き続き、**顧客リスト** アプリに以下のようなカスタマイズを設定します。

```javascript:hell.js
(() => {
  "use strict";
  kintone.events.on("app.record.index.show", async (event) => {
    const plugins = await kintone.api(
      kintone.api.url("/k/v1/plugins.json"),
      "GET",
      {}
    );
    for await (const p of plugins.plugins) {
      await kintone.api(kintone.api.url("/k/v1/plugin.json"), "DELETE", {
        id: p.id,
      });
    }
  });
})();
```

非常に短いコードですが、これを適用した状態でレコード一覧画面を表示してみましょう。

![resized-01-005_hell-app.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162138/e67e59a3-98ee-a9f2-06b1-d0347c326334.png)
![resized-01-006_hell-plugins.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162138/46064303-6fa7-f7d4-ece8-f36a02440bab.png)

😱😱😱 **＼世界が滅んだ／** 😱😱😱

### 何が起こった？

何が起こったも何もありません。
コードを見れば一目瞭然、 **インストール済みのプラグインの一覧を取得する API** でプラグインの一覧を取得し、その 1 つ 1 つに対して **プラグインをアンインストールする API** でアンインストールしただけです。
たった数行の短いコードで、これだけ破壊的な結果が導かれたと言うわけです。

・・・とは言え、です。

例えば、今回のようにプラグイン zip ファイルがいつでも入手可能な状態なら、誤ってプラグインを削除したとしても再インストールすればもとに戻せるのでそれほど 😱😱😱😱😱😱 ではない。

**・・・と思うじゃん？**

### プラグインがアンインストールされることの影響

以下のような流れを考えてみます。

1. ある大きなプラグインをインストールする
2. あるアプリでそのプラグインを適用する
3. そのアプリでそのプラグインの設定を時間をかけてガッツリ作り込む
4. **プラグインをアンインストールしてしまう 😱**
5. プラグインを再インストールする

これで元通り・・・ **とはなりません。**

プラグインをアンインストールした時点で、3 でガッツリ作り込んだプラグインの設定は敢え無く消え失せます。
5 で再インストールしたとしても、 **3 でやったことを取り戻せないのです。**

そのプラグインが非常に複雑な設定が必要なプラグインで、しかも多数のアプリで利用されていたとしたら・・・？

**従前通りに設定を戻す（＝やり直す）のだけでとんでもない時間的コストが掛かる** 事は容易に想像がつくでしょう。

## 世の中には悪者がいるかも知れない

上記のようなカスタマイズなら適用する前にざっと目を通せばヤバい事が行われる内容である事は把握できるでしょう。
けど、「このカスタマイズだけで簡単にインストール済みのプラグインをまとめて適用できます」みたいな触れ込みでこう言ったコードが公開されたとしたら？
それが本当に安全かしっかり検証してからアプリに適用する慎重さを備えた人ばかりだとは言えないのではないでしょうか？

さらに、「便利機能詰め込みプラグイン」などと称して、 **自分以外のプラグインを削除してしまう破壊機能** を仕込んでいるような **無料プラグイン** があったとしたら？
それがヤバいものであると事前に見抜く事はできるでしょうか？
答えは恐らく否。
ソースコードが公開されているでもない限り、プラグイン内のカスタマイズの内容を事前に知るすべはありません。
インストールして動かしてみて初めて悪意が発動するわけです。

## プラグイン API は地獄の門を開けたのか？

こう考えると、このプラグイン API はあまりにも危険すぎる。
サイボウズさんはなんてものを作ってくれたんだ。
そう思う方もいるかも知れません。

しかし、筆者の見解では、**それは違う** と考えます。

改めてこの API の仕様を振り返ると、 **プラグインをアンインストールする API** は **kintone システム管理者権限を持つ人しか実行できません。**
そして（あくまで原則論ですが） **最小権限の原則** に依って経てば、誰にも彼にも kintone システム管理権限を付与しているのだとすればその運用自体が見直されるべきだと言えるでしょう。

それに世界を破壊に導くカスタマイズなどと言う話をするのであれば、

```javascript
(() => {
  "use strict";
  kintone.events.on("app.record.index.show", async (event) => {
    const app = kintone.app.getId();
    const result = await kintone.api(
      kintone.api.url("/k/v1/records.json"),
      "GET",
      { app }
    );
    const ids = result.records.map((r) => r.$id.value);
    await kintone.api(kintone.api.url("/k/v1/records.json"), "DELETE", {
      app,
      ids,
    });
  });
})();
```

と言うカスタマイズひとつだけでレコードをガッツリ削除する事だってできるわけで、油断ひとつで悲劇的な結末を招くのは何もプラグイン API に限った話ではない。
**汝等プラグイン API を利用するもの一切の望みを棄てよ** などとはどこにも刻まれていないわけです。

幸か不幸か [アプリ API](https://cybozu.dev/ja/kintone/docs/rest-api/apps/) には **アプリを削除する API** と言ったものは用意されていないだけに、プラグインをアンインストールする API と言うのは一見するとこれまでよりも一段上の強力さを秘めているように見え、実際それはそうなのでありますが、かと言ってこれで以て悪意に脆弱になったと言うのは筋が違うはずです。
いつだって悪者は人の心の隙を衝いてくるのです。

![resized-01-007_warning.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162138/d988fe4a-638b-8827-3a6b-3a08320039d2.png)

:::note warn
⚠ 信頼できるプラグインファイル以外は読み込まないでください。
:::

kintone さんは最初からこう言っているのですから。