Tauriのプラグインとpermissionについて

# 1. この記事について


## 1-1. 内容


Tauriでは行うバックエンド処理のプラグインが用意されていて、これを呼び出して使う事でファイル操作などを簡単に実装することができますが、使用するプラグインの許可、プラグインがアクセスして良いディレクトリの許可をそれぞれ明示的に指定する必要があります


この記事では何故これが必要なのか解説し、どのように設定すれば良いのかまとめていきます


## 1-2. 前提知識


先日入門記事を書いていますので、こちらの内容を知っている前提で書いています


https://qiita.com/studio_haneya/items/ee7b040cd9e0ad2503c6


# 2. プラグインのPermissionについて


## 2-1. バックエンドとフロントエンド


Tauriアプリはウェブアプリ開発を模して、ファイル処理などを記述するバックエンド部分と、ユーザーが操作するUIを記述するフロントエンド部分に分割して書く設計になっています。バックエンド部分はRustで記述するので、バグや不整合が起こりにくく処理速度も速いというメリットがあり、フロントエンド部分はReact, VueなどをTypeScriptで記述できる為、リッチなGUIでグラフィカルな表現が可能になっており、インタラクティブな処理を書きやすくなっていたり、ウェブアプリのフロントエンドを流用しやすくなっていたりといったメリットがあります


## 2-2. WebViewとXSS


このフロントエンド部分はWebViewを使って実装されているので、OS標準の高性能な最新ブラウザの機能が使えますが、一方でXSSなどの脆弱性が入り込む余地もあります。DBから取得した文字列やユーザーが入力したファイルなどからJavaScriptのコードが実行され、ローカルのファイルの内容を転送されてしまうようなケースが想定できる為、XSS対策が必要であり、これがやりやすいようにプラグインのPermissionが設定されているわけです


なお、フル機能のブラウザが動作するという点でChromiumを使って動作するElectronにもXSSのリスクがあります。ElectronはTauriのようなローカルディレクトリへの厳密なアクセスコントロールもありませんし、Chromiumを同梱して配布するので更新を怠ると古いChromium由来の脆弱性のリスクもあり、OS更新に伴いWebViewも更新されるTauriと比べてリスクが大きくなります


React NativeやFlutterのようにWebViewを使わずに動作するネイティブコードがビルドされるフレームワークであればXSSのリスクはなくなりますが、TauriはPermissionによるアクセスコントロールによりXSSのリスクを最小化する設計になっています


## 2-3. プラグインとPermission


上で書いたXSSによる被害を防ぐため、ファイル操作などの標準バックエンド機能プラグインの実行を許可するか、ファイルの読み書きをどのディレクトリに対して許可するか、それぞれ明示的に設定する設計になっています。Tauriで標準機能として提供されているプラグインは攻撃者も存在を知っていますから、これが狙われるだろうという考え方だと思われます


一方、自作したバックエンドの関数をinvokeして使う場合は特にPermissionは必要ありません。知らないものは狙われにくいですし、必要な安全措置は開発者自身が行うべきという考え方でしょう。この辺りもウェブアプリのバックエンドとフロントエンドの役割分担と似ていて、ウェブ開発者からすると理解しやすい設計だと思います


# 3. プラグインの使い方


## 3-1. プラグインの使用方法


### インストール


まず環境にプラグインをインストールします


```terminal:terminal
npm run tauri add dialog
```


### 使用する


ダイアログを開いてファイルを選択するdialogプラグインを使用します


import { open } from "@tauri-apps/plugin-dialog"しておいて
await open()すると選択したファイルのPATHを文字列で取得できます



```react:/src-tauri/App.tsx
import { useState } from "react";
import { open } from "@tauri-apps/plugin-dialog";
import "./App.css";

export default function App() {
  const [filePath, setFilePath] = useState("");

  const handleOpenFile = async () => {
    const selected = await open({
      multiple: false,
      directory: false,
    });
    if (selected) {
      setFilePath(String(selected));
    }
  };
  return (
    <main className="container">
      <h1 className="text-red-500 text-5xl">Welcome to Tauri + React</h1>
      <button
        className="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
        onClick={handleOpenFile}
      >
        ファイルを選択
      </button>
      <div className="mt-4 bg-gray-100 p-2 rounded">{filePath}</div>
    </main>
  );
}
```


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/aa8c80c6-fea7-43d4-855f-13f1bba0a847.png" width=40%>


## 3-2. Permissionの指定方法


### バージョンごとの違い


プラグインごとのPermission、ファイル読み書きを許可するディレクトリを指定するPermissionはv1から存在しましていましたが、記載方法と設定ファイルの位置がv1とv2で大きく変わっていますので注意が必要です


### v1の書き方


設定はすべて/src-tauri/tauri.conf.jsonに記載します


Permissionとしては以下を指定します
・プラグインの機能単位の可否
・プラグイン単位でのアクセス可能ディレクトリの指定


```json:/src-tauri/tauri.conf.json
{
  "tauri": {
    "allowlist": {
      "fs": {
        "readFile": true,
        "writeFile": true,
        "scope": ["$APP", "$DOCUMENT"]
      }
    }
  }
}
```


### v2の書き方


設定は/src-tauri/capability/*.jsonに分割記載できるようになりました


https://v2.tauri.app/security/capabilities/


/src-tauri/tauri.conf.json に書くことも出来ますが、公式ドキュメントでも /src-tauri/capability/default.json に設定する方法が書かれていますのでそちらを推奨しているようです


```:ディレクトリ構造
src-tauri/
├── tauri.conf.json               # エントリーポイント
├── capability/
│   ├── default.json              # 全体設定や共通scope
│   ├── fs.json                   # ファイルアクセスに関する権限
│   ├── shell.json                # シェル操作に関するscope
│   ├── http.json                 # ネットワーク通信に関するscope
```


Permissionとしては以下を指定します
・プラグインの機能単位の可否
・プラグインの機能単位のアクセス可能ディレクトリの指定


プラグイン単位でなく機能単位でアクセス可能なディレクトリを指定するようになっていますので、読み込みのみOK、書き込みはNGとするような指定ができます


```json:/src-tauri/capability/default.json
{
  "permissions": [
    "core:default",
    "opener:default",
    "dialog:default",

    {
      "identifier": "fs:allow-exists",
      "allow": [
        { "path": "**" }
      ]
    },

    {
      "identifier": "fs:allow-app-read",
      "allow": [
        {
          "path": "$DOCUMENT/**"
        }
      ]
    },

    {
      "identifier": "fs:allow-app-write",
      "allow": [
        {
          "path": "$DOWNLOAD/**"
        }
      ]
    }
  ]
}
```


なお、プラグインをインストールだけしている状態では "dialog:default" のように指定されています。dialogはデフォルト設定のままで使用できますが、fsのようにディレクトリの許可を与えないと使えないプラグインもあります。ファイルやDBへの変更を可能とするプラグインが明示的な許可を要求する設計になっているようです


## 3-3. 標準プラグインいろいろ


公式ドキュメントにまとめてくれています


https://v2.tauri.app/plugin/


いくつか紹介していこうと思います


### dialog


ファイルダイアログからファイル選択をしたり、window.confirmのようなダイアログを出したりという機能を提供するプラグインです。Windowsの場合はWebViewがEdgeなので、Edgeのダイアログと同じデザインのものが開きます


```react:/src-tauri/App.tsx
import { useState } from "react";
import { open } from "@tauri-apps/plugin-dialog";
import "./App.css";

export default function App() {
  const [filePath, setFilePath] = useState("");

  const handleOpenFile = async () => {
    const selected = await open({
      multiple: false,
      directory: false,
    });
    if (selected) {
      setFilePath(String(selected));
    }
  };
  return (
    <main className="container">
      <h1 className="text-red-500 text-5xl">Welcome to Tauri + React</h1>
      <button
        className="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
        onClick={handleOpenFile}
      >
        ファイルを選択
      </button>
      <div className="mt-4 bg-gray-100 p-2 rounded">{filePath}</div>
    </main>
  );
}
```


### fs


Nodeのfsとほぼ同じ機能で、ファイルを扱うプラグインです
ファイルの存在確認、ファイルの読み書きなどができます


```react:/src-tauri/App.tsx
import { useState } from "react";
import { exists, readTextFile } from "@tauri-apps/plugin-fs"
import "./App.css";

export default function App() {
  const [fileContent, setFileContent] = useState<string>("");

  const handleReadFile = async () => {
    const filePath = "C:/Users/hoge/ドキュメント/hoge.txt";
    try {
      const isExist = await exists(filePath);
      if (!isExist) {
        setFileContent("ファイルが存在しません。");
        return;
      }
      const content = await readTextFile(filePath);
      setFileContent(content);
    } catch (error) {
      setFileContent("ファイルの読み込み中にエラーが発生しました。");
    }
  };

  return (
    <main className="container">
      <button
        className="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
        onClick={handleReadFile}
      >
        クリックすると文字列ファイルを読み込みます
      </button>
      <div className="p-2">
        {fileContent}
      </div>
    </main>
  );
}
```


### path


nodeのpathっぽい機能で、実行ファイルのPATHを取得したり、PATHをjoinしたりできます


```tsx:/tauri-src/App.tsx
import { join, resourceDir } from "@tauri-apps/api/path"

const resourceDir = await resourceDir()  // 実行しているexeのPATHを返す
const configDir = await join(resourceDir, "config.json")  // <実行パス>/config.json を返す
```


### sql


MYSQL、PostgreSQL、SQliteに対応しているようです


ネイティブアプリで扱うことを考えるとSQliteでローカルに情報を保持するような使いかたがメインになるのではないかと思います


```
import Database from '@tauri-apps/plugin-sql';
const path = "path/to/hoge.sqlite"

const db = await Database.load(`sqlite:${path}`);

await db.execute('INSERT INTO ...');
const result = await db.select("SELECT * FROM ...")
```


### store


jsonファイル形式でKey-Valueストアを構築できます


```
import { load } from "@tauri-apps/plugin-store"
import { join, resourceDir } from "@tauri-apps/api/path"

const configDir = await join(await resourceDir(), "config.json"

const store = await load(configDir, { autoSave: false });

const data = await store.get<string>('data');

await store.set('data', "test")
await store.save()
```


# 4. まとめ


何故ネイティブアプリを作っているのにPermissionエラーが出てくるのかと戸惑いましたが、理由が分かると納得です。標準プラグインの種類も多いので、上手く使いこなしてセキュアなアプリを開発していきたいです


# 5. 参考


https://v2.tauri.app/security/


https://qiita.com/Odashikonbu/items/337e8110ed5db0e732a7