Sik.limited Logo

Tauri v2の最小権限設計: capability・permission・scopeを分ける

Tauri v2でウィンドウ、プラグイン、ファイル経路を最小権限で設計するために、capability、permission、scopeの役割と実装を解説します。

Sik ·

権限設定は機能一覧ではなく、信頼境界の地図だ

Tauri v2の権限設定を「動かない機能を通すJSON」として扱うと、default権限を重ね、いつの間にか画面が端末の広い領域へ到達するアプリになります。設定は、どのウィンドウが、どのプラグインまたはcommandを、どんな入力と対象範囲で呼べるかを記録する信頼境界の地図です。

最小権限は機能を減らすことではありません。ユーザーが必要な作業を完了できる一方、不要な画面、経路、操作には権限を与えないことです。ファイル保存、外部URL、クリップボード、通知、複数ウィンドウを追加するたび、この地図を更新します。

capability、permission、scopeを一文ずつ区別する

三つは似て見えますが、答える問いが異なります。

概念問い
capabilityどのウィンドウ/WebViewへ、どの権限セットを渡すかmainだけにノート編集の機能を与える
permissionそのプラグイン/APIのどの操作を許すかdialog:allow-open、テキストファイル読取り
scope許した操作は、どの値・パス・URLまでに及ぶか$APPDATA/notes/*.mdだけ書込み

capabilityは配布先、permissionは動詞、scopeは対象の範囲です。fs:allow-write-text-fileを許しても、scopeで書込み先を絞らなければ最小権限ではありません。逆にscopeだけ細かくても、外部ログイン画面へそのcapabilityを付ければ不要な到達経路が生まれます。

最も小さい機能単位から始める理由

「ノートを保存する」ではなく、保存フローを分けます。ユーザーが保存先を選ぶにはdialog、アプリ管理フォルダーへ下書きを書くにはfs、作成済みノートの一覧を出すには読取りが必要です。それぞれを分けると、キャンセル、拒否、既存ファイル、外部パスの失敗をUIで説明できます。

選ぶ → dialog権限
読込む → fs read権限 + 読取りscope
保存する → fs write権限 + 書込みscope
一覧にする → directory read権限 + 限定scope

開発時に「一旦すべて許可」は便利に見えても、本番で必要な範囲を見失わせます。最小の一操作を通し、必要になったpermissionとscopeだけを足す方が、設定レビューも障害調査も容易です。

capabilityはウィンドウごとに分け、重複適用は意識的に行う

メイン編集画面、環境設定、ヘルプ、OAuthログインのウィンドウには同じ能力が必要とは限りません。とくに外部コンテンツを表示するウィンドウにローカルファイルやshellの権限を付けません。

// src-tauri/capabilities/main-editor.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-editor",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "dialog:allow-open",
    "dialog:allow-save",
    { "identifier": "fs:allow-read-text-file", "allow": [{ "path": "$APPDATA/notes/*.md" }] },
    { "identifier": "fs:allow-write-text-file", "allow": [{ "path": "$APPDATA/notes/*.md" }] }
  ]
}
// src-tauri/capabilities/help.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "help-window",
  "windows": ["help"],
  "permissions": ["core:default"]
}

同じpermissionを複数capabilityへ置く場合も、「両方に必要だから」と書ける状態にします。window labelはtitleと別の安定した識別子として管理し、ユーザー入力から生成しません。

permissionはコマンド名でなく影響範囲として読む

permission識別子は機能名に見えますが、実際にはOSへの影響を持ちます。fs:default、shell実行、任意URLを開くpermissionを追加する前に、誰がどの入力で何を実行できるかを書きます。プラグイン版によって識別子やschemaは変わり得るため、導入済みバージョンの生成schemaと公式文書を必ず確認してください。

要求よくある広すぎる許可狭い代替
ノート保存ファイルシステムのdefaultテキスト書込みとnotes配下scope
URLを開く任意URLをアプリ内表示固定originの許可、その他は既定ブラウザ
ファイル選択読書き一式dialog openだけを先に許可
設定保存ホームディレクトリ全体アプリデータ配下の専用ファイル

scopeは許可の後に来る二つ目の質問だ

permissionが「書けるか」を答えるなら、scopeは「どこに書けるか」を答えます。パスscopeにはOS差、環境変数、ワイルドカード、シンボリックリンク、取消後のパスを含む現実の入力があります。開発者のホームで通るパスが、別ユーザー、別OS、サンドボックス、外部ドライブでも安全とは限りません。

{
  "identifier": "fs:allow-write-text-file",
  "allow": [{ "path": "$APPDATA/notes/*.md" }]
}

アプリがユーザー選択の任意ファイルを直接編集するなら、選択済みパスの再利用、拡張子・サイズ検証、削除・移動時のUI、パス正規化を明示します。scopeを広げることが唯一の解決ではありません。import時にアプリ領域へ複製し、内部データだけを編集すれば、バックアップや同期の境界も明快になります。

自作Rust commandは別の製品APIとして扱う

#[tauri::command]はフロントエンドから呼べるローカルAPIです。Rustで実装したから安全なのではなく、入力検証、認可、パス、エラー、ログを考える必要があります。

use std::path::{Path, PathBuf};

#[tauri::command]
fn save_note(app: tauri::AppHandle, name: String, body: String) -> Result<(), String> {
    if !name.ends_with(".md") || name.contains('/') || name.contains('\\') {
        return Err("Invalid note name".into());
    }
    if body.len() > 1_000_000 { return Err("Note is too large".into()); }
    let base = app.path().app_data_dir().map_err(|e| e.to_string())?.join("notes");
    let target: PathBuf = base.join(name);
    if !target.starts_with(&base) { return Err("Invalid path".into()); }
    std::fs::create_dir_all(&base).map_err(|e| e.to_string())?;
    std::fs::write(target, body).map_err(|e| e.to_string())
}

commandを追加したら、呼べるwindow、入力の最大量、失敗時にユーザーへ返す文言、診断ログに残す内容をレビューします。任意パス、任意SQL、任意shellコマンドのような汎用入口を作らず、製品操作ごとに狭いcommandを作ります。

# src-tauri/permissions/notes.toml
[[permission]]
identifier = "allow-save-note"
description = "Allows saving a validated note in the app data directory."
commands.allow = ["save_note"]

リモートコンテンツにローカル権限を既定で与えない

外部ヘルプ、OAuth、ユーザー生成HTML、Webページを表示するWebViewは、アプリ自身の編集画面と異なる信頼境界です。リモートコンテンツにmain capabilityを渡さないことを既定にし、必要な認証は既定ブラウザまたは権限を持たない専用windowで処理します。外部URLを開くAPIも、許可originと戻りURLを絞ります。

この原則はXSSや依存パッケージの脆弱性が起きた場合の被害範囲を狭めます。デザイン上同じ見た目でも、表示するコンテンツの出所が違えばcapabilityも違うべきです。

モバイルも視野に入れるならplatform分離から始める

Tauriのモバイル対応を検討しても、デスクトップのファイル、トレイ、複数windowをそのまま移植できるとは限りません。UI、ドメイン型、APIクライアントは共有し、権限とOS操作はplatform adapterへ分離します。Capacitorを併用する場合も、カメラ、プッシュ、共有はモバイル固有の契約として扱います。

core/       ドメイン、検証、API
ui/         画面と状態
desktop/    Tauri command、capability、ファイル操作
mobile/     カメラ、通知、モバイル権限

権限エラーを解く順番が権限品質を決める

not allowedを見たら次の順で調べます。

  1. 呼出しているwindow labelに期待するcapabilityが適用されているか。
  2. 必要なプラグインpermissionが、導入済み版の識別子で与えられているか。
  3. 操作対象のパス、URL、入力がscopeに一致するか。
  4. dialogで選ぶ行為とfsで読書きする行為を混同していないか。
  5. それでも必要なら、製品要件を記録して最小のpermissionまたはscopeを追加できるか。

全許可を追加してエラーを消すのは、問題を先送りするだけです。失敗ケースをテストに残せば、新機能やプラグイン更新で境界が広がったことも検出できます。

配布前の最小権限チェックリスト

  • 各capabilityに対象windowと目的が書かれているか。
  • ファイル、URL、shell、クリップボードの権限は必要な操作・対象だけか。
  • 外部/ユーザー生成コンテンツのwindowはローカル権限を持たないか。
  • custom commandは入力、パス、サイズ、状態をRust側で検証するか。
  • macOS、Windows、Linuxの対応範囲で、拒否、取消、パスなし、移動済みファイルを試したか。
  • 誰がpermission追加をレビューし、リリース時に生成schemaを再確認するか決めたか。

セキュリティは権限ファイルだけで完成しない

capabilityは重要ですが、更新署名、依存関係の更新、CSP、認証トークンの保管、ログの秘匿、データ移行、OS別QAも必要です。権限ファイルはこれらを代替しません。ただし、端末能力の入口を明示して狭く保てるため、他の対策と合わせたとき被害範囲を管理しやすくなります。

出典

最新の記事