frappe-impl-whitelisted について
Frappe 開発者向けに、ホワイトリスト登録 API の実装手順をまとめたガイドです。エンドポイント設計、権限、セキュリティ、エラー処理に加え、v14、v15、v16 の違いも扱います。
主な機能
- 01エンドポイント種別と権限モデルの選択
- 02認証 API とゲスト API の実装手順
- 03コントローラーメソッドとファイルアップロード
- 04バックグラウンドジョブとエラー処理のパターン
- 05クライアント呼び出しと外部連携の例
- 06v14~v16 の機能差
活用シーン
- 01公開 API またはログインユーザー向け API の設計
- 02DocType フォーム用コントローラーメソッドの実装
- 03ゲスト API の入力検証とレート制限の確認
- 04Server Script API からホワイトリスト登録メソッドへの移行
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-impl-whitelistedインストール後、Claude Code で/frappe-impl-whitelistedを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/anti-patterns.mddecision-tree.mdexamples.mdworkflows.md詳細
Frappe のホワイトリスト登録メソッドの実装ワークフロー
API エンドポイントを構築するためのステップバイステップのワークフローです。デコレーターの構文については、frappe-syntax-whitelistedを参照してください。
バージョン:v14/v15/v16(バージョン固有の機能には注記があります)
基本の判断:どの種類のエンドポイントか?
WHAT ARE YOU BUILDING?
│
├─► Public API (no login required)?
│ └─► allow_guest=True + STRICT input validation + rate limiting
│
├─► Authenticated API for logged-in users?
│ └─► Default @frappe.whitelist() + document permission checks
│
├─► Admin-only API?
│ └─► frappe.only_for("System Manager")
│
├─► Document-specific method (called from form)?
│ └─► Controller method + frm.call() from JS
│
├─► Standalone utility API?
│ └─► Separate api.py + frappe.call() from JS
│
├─► External webhook receiver?
│ └─► allow_guest=True + signature verification
│
└─► Background job trigger?
└─► Authenticated API that calls frappe.enqueue()
ワークフロー 1:エンドポイントを設計する
ステップ 1:配置場所を選ぶ
WHERE SHOULD THE CODE LIVE?
│
├─► Related to a DocType, called from its form?
│ └─► doctype/xxx/xxx.py (controller method)
│ Client: frm.call('method_name', args)
│
├─► Related to a DocType, standalone?
│ └─► doctype/xxx/xxx_api.py or myapp/api/module.py
│ Client: frappe.call('myapp.api.module.method')
│
├─► General app utility?
│ └─► myapp/api.py (small app) or myapp/api/module.py (large app)
│
└─► External integration?
└─► myapp/integrations/service_name.py
ステップ 2:権限モデルを選ぶ
WHO CAN CALL THIS API?
│
├─► Anyone (public) → allow_guest=True
│ ⚠️ MUST validate ALL input, sanitize for XSS, rate limit
│
├─► Any logged-in user → Default (no allow_guest)
│ Still check document permissions per record!
│
├─► Specific role(s) → frappe.only_for("Role")
│
└─► Document-level → frappe.has_permission(doctype, ptype, doc)
ステップ 3:HTTP メソッドを選ぶ
WHAT DOES THE API DO?
│
├─► Read-only → methods=["GET"]
├─► Creates/modifies data → methods=["POST"]
└─► Both or default → omit methods parameter (all allowed)
ワークフロー 2:認証済み API を実装する
手順
ステップ 1:関数を作成する
# myapp/api.py
import frappe
from frappe import _
@frappe.whitelist()
def get_customer_balance(customer):
"""Get outstanding balance for a customer."""
# 1. Permission check
if not frappe.has_permission("Customer", "read", customer):
frappe.throw(_("Not permitted"), frappe.PermissionError)
# 2. Validate input
if not customer or not frappe.db.exists("Customer", customer):
frappe.throw(_("Customer not found"), frappe.DoesNotExistError)
# 3. Fetch and return
balance = frappe.db.sql("""
SELECT COALESCE(SUM(outstanding_amount), 0)
FROM `tabSales Invoice`
WHERE customer = %s AND docstatus = 1
""", customer)[0][0]
return {"customer": customer, "balance": balance}
ステップ 2:クライアントスクリプトから呼び出す
frappe.call({
method: 'myapp.api.get_customer_balance',
args: { customer: 'CUST-00001' },
callback(r) {
if (r.message) console.log(r.message.balance);
}
});
ステップ 3:curl でテストする
# Authenticate first
curl -X POST https://site.com/api/method/login \
-d 'usr=admin&pwd=password'
# Call the API
curl -X POST https://site.com/api/method/myapp.api.get_customer_balance \
-H "Content-Type: application/json" \
-d '{"customer": "CUST-00001"}' \
--cookie cookies.txt
# Or use token auth
curl -X POST https://site.com/api/method/myapp.api.get_customer_balance \
-H "Authorization: token api_key:api_secret" \
-H "Content-Type: application/json" \
-d '{"customer": "CUST-00001"}'
ワークフロー 3:パブリック(ゲスト)API を実装する
手順
ステップ 1:厳格な検証を行う関数を作成する
@frappe.whitelist(allow_guest=True, methods=["POST"])
def submit_inquiry(name, email, phone=None, message=None):
"""Public contact form — strict validation required."""
# 1. Validate required fields
if not all([name, email]):
frappe.throw(_("Name and email are required"))
# 2. Validate email format
if not frappe.utils.validate_email_address(email):
frappe.throw(_("Invalid email address"))
# 3. Sanitize ALL input
name = frappe.utils.strip_html(name)[:100]
email = email.strip().lower()[:200]
phone = frappe.utils.strip_html(phone)[:20] if phone else None
message = frappe.utils.strip_html(message)[:2000] if message else None
# 4. Create record with ignore_permissions
lead = frappe.get_doc({
"doctype": "Lead",
"lead_name": name, "email_id": email,
"phone": phone, "notes": message, "source": "Website"
})
lead.insert(ignore_permissions=True)
return {"success": True, "message": _("Thank you")}
ステップ 2:レート制限を追加する(v15 以降)
from frappe.rate_limiter import rate_limit
@frappe.whitelist(allow_guest=True, methods=["POST"])
@rate_limit(limit=5, seconds=60) # 5 calls per minute
def submit_inquiry(name, email, phone=None, message=None):
...
ゲスト API の重要なルール
- 必ずすべての入力パラメーターを検証し、サニタイズしてください
- 必ずデータを書き込むエンドポイントには
methods=["POST"]を使用してください - 必ずレート制限を追加してください(v15 以降ではデコレーター、v14 ではキャッシュベースの手動スロットリング)
- 決して内部エラーの詳細を公開しないでください。
frappe.log_error()でログに記録し、汎用的なメッセージを表示してください - 決して機密データ(内部 ID、ファイルパス、スタックトレース)を返さないでください
- 決して未加工のユーザー入力を
frappe.get_doc()に渡さないでください。明示的なフィールドマッピングを使用してください
ワークフロー 4:コントローラーメソッドを実装する
手順
ステップ 1:DocType コントローラーにメソッドを追加する
# myapp/doctype/sales_order/sales_order.py
class SalesOrder(Document):
@frappe.whitelist()
def calculate_shipping(self, carrier):
"""Called from form via frm.call()."""
if not self.shipping_address:
frappe.throw(_("Shipping address required"))
rate = get_shipping_rate(self.shipping_address, carrier)
return {"carrier": carrier, "rate": rate}
ステップ 2:フォーム JS から呼び出す
frm.call('calculate_shipping', {
carrier: 'FedEx'
}).then(r => {
frm.set_value('shipping_amount', r.message.rate);
});
主な違い:frm.call()経由で呼び出されるコントローラーメソッドでは、Frappe がドキュメントの権限を自動的に確認します。ドキュメント自体に対する手動の権限確認は不要です。
ワークフロー 5:エラー処理を実装する
標準パターン
@frappe.whitelist()
def process_payment(invoice, amount):
try:
# Validate
if not invoice:
frappe.throw(_("Invoice required"), frappe.ValidationError)
if not frappe.has_permission("Sales Invoice", "write", invoice):
frappe.throw(_("Not permitted"), frappe.PermissionError)
# Process
result = do_payment(invoice, float(amount))
return {"success": True, "data": result}
except (frappe.ValidationError, frappe.PermissionError):
raise # Let Frappe handle (417 / 403)
except frappe.DoesNotExistError:
frappe.throw(_("Not found"), frappe.DoesNotExistError) # 404
except Exception:
frappe.log_error(frappe.get_traceback(), "Payment Error")
frappe.local.response["http_status_code"] = 500
return {"success": False, "error": _("Internal error")}
HTTP ステータスコードのリファレンス
| コード | Frappe の例外 | 発生する状況 |
|---|---|---|
| 200 | — | 成功 |
| 403 | PermissionError | アクセス拒否 |
| 404 | DoesNotExistError | 見つからない |
| 409 | DuplicateEntryError | 重複 |
| 417 | ValidationError | 検証失敗 |
| 429 | — | レート制限超過(v15 以降) |
| 500 | Exception | サーバーエラー |
ワークフロー 6:ファイルアップロードのエンドポイント
@frappe.whitelist()
def upload_attachment(doctype, docname):
"""Handle file upload attached to a document."""
if not frappe.has_permission(doctype, "write", docname):
frappe.throw(_("Not permitted"), frappe.PermissionError)
file = frappe.request.files.get('file')
if not file:
frappe.throw(_("No file provided"))
file_doc = frappe.get_doc({
"doctype": "File",
"file_name": file.filename,
"attached_to_doctype": doctype,
"attached_to_name": docname,
"content": file.read(),
"is_private": 1
})
file_doc.insert(ignore_permissions=True)
return {"file_url": file_doc.file_url}
ワークフロー 7:バックグラウンドジョブのエンドポイント
@frappe.whitelist()
def start_heavy_export(doctype, filters=None):
"""Trigger a background job — returns immediately."""
frappe.only_for("System Manager")
frappe.enqueue(
"myapp.tasks.export_data",
queue="long",
timeout=1500,
doctype=doctype,
filters=filters,
user=frappe.session.user
)
return {"status": "queued", "message": _("Export started")}
クライアント統合パターン
オプションを指定した frappe.call()
frappe.call({
method: 'myapp.api.my_method',
args: { param: 'value' },
freeze: true,
freeze_message: __('Processing...'),
callback(r) { console.log(r.message); },
error(r) { frappe.msgprint(__('Error')); }
});
Async/Await パターン
const r = await frappe.call({
method: 'myapp.api.get_data',
args: { id: 123 }
});
console.log(r.message);
外部システムからの REST API
# Token auth (recommended for integrations)
curl -H "Authorization: token api_key:api_secret" \
https://site.com/api/method/myapp.api.method
# Bearer auth (OAuth)
curl -H "Authorization: Bearer access_token" \
https://site.com/api/method/myapp.api.method
セキュリティルール(必ず行うこと/決して行わないこと)
- 必ずドキュメントにアクセスする前に権限を確認してください
- 必ずパラメーター化クエリを使用してください。SQL で f-string を決して使用しないでください
- 必ずゲスト API の入力を検証し、サニタイズしてください
ignore_permissions=Trueを使用する前に、必ずfrappe.only_for()でロールを確認してください- 内部エラーを決して公開しないでください。詳細をログに記録し、汎用的なメッセージを返してください
- データを変更するエンドポイントで
methods=["GET"]を決して使用しないでください - ゲスト API からの
dataディクショナリを決して信頼しないでください。明示的なパラメーター名を使用してください - ブラウザーからの fetch() 呼び出しには、必ず
X-Frappe-CSRF-Tokenヘッダーを含めてください
移行:Server Script API からホワイトリスト登録メソッドへ
| 手順 | 操作 |
|---|---|
| 1 | Server Script のロジックをmyapp/api/module.pyにコピーする |
| 2 | 同じ権限モデルで@frappe.whitelist()デコレーターを追加する |
| 3 | すべてのfrappe.call()参照を新しいドット区切りパスに更新する |
| 4 | Server Script を無効化または削除する |
| 5 | bench --site sitename migrateを実行する |
バージョンの違い
| 機能 | v14 | v15 | v16 |
|---|---|---|---|
| @frappe.whitelist() | はい | はい | はい |
| allow_guest | はい | はい | はい |
| methods パラメーター | はい | はい | はい |
| 型アノテーションの検証 | いいえ | はい | はい |
| @rate_limit デコレーター | いいえ | はい | はい |
| API v2 エンドポイント | いいえ | はい | はい |
v15 以降の型検証
@frappe.whitelist()
def typed_api(customer: str, limit: int = 10) -> dict:
"""v15+ validates types from annotations automatically."""
return {"customer": customer, "limit": limit}
参考ファイル
| ファイル | 内容 |
|---|---|
| decision-tree.md | API の種類と権限の選択に関する完全なガイド |
| workflows.md | 実装パターンのステップバイステップガイド(10 以上のワークフロー) |
| examples.md | 本番環境ですぐに使えるコード例 |
コメント
Markdown 対応