権限で出し分ける
ロールに応じてボタン・列・項目・メニューを出す/出さない。
ロールで出し分けたい要素に roles を書く。書いたロールのどれかを持っている人にだけ見える。
- { field: salary, label: 給与, type: number, roles: [hr, manager] }
actions:
- { id: export, type: plugin, plugin: csvExport, label: CSV出力, roles: [admin] }roles を書かなければ全員に見える。空配列も同じ扱い。
6か所に書ける
| 書ける場所 | 効果 |
|---|---|
field | その入力項目・表示項目が見えない |
column | 一覧のその列が出ない |
action | そのボタンが出ない(行のボタンも。組み込みの編集・削除は type: edit / type: delete の宣言に書く) |
dashboardItem | そのカードが出ない |
menuItem | メニューのその項目・グループが出ない |
| 画面そのもの(0.9.22) | メニューからその画面へ行く項目が消え、直に開いても中身が出ない |
画面ごと見せたくないなら、画面そのものに書く。
- type: master
id: price_master
title: 単価マスタ
repository: priceRepository
roles: [admin]メニューの項目に書かなくても、行き先の画面が admin だけなら項目は消える(押しても「権限がありません」と出る項目は、壊れているのと同じに読まれるので)。URL で直に来た人には「この画面を開く権限がありません」とだけ出して、読み込みも始めない。Flutter・Vue・React で同じ。
0.9.21 まではページに書けなかったので、メニュー項目に書くしかなかった。メニューで隠しても URL で来られるし、サーバは同じことを役割名で手書きするしかなかった。
「この画面は誰が開けるか」は辿らないと分からない
画面を開ける人は入口から辿る。メニュー項目が admin だけなら、その先の画面も admin だけ。そこにあるボタンが誰でも押せる形で書いてあっても、来られるのが admin だけなら開けるのは admin だけ。最後に、画面そのものの roles を門として掛ける(入口を通ってきた人のうち、持っている人だけ)。
1枚の定義を読んでも出ない値なので、道具に聞く。
$ npx -p @hatake-fw/api hatake explain app.yaml
## 画面を開ける人
・受注照会(order_search) … 誰でも開ける
・顧客マスタ(customer_master) … admin だけ
・単価マスタ(price_master) … 誰も開けない(入口の権限が食い違っている)最後の行のような誰も開けない画面は、定義としては通るし、画面を見ても気づけない(staff の画面に admin 限定のボタンでだけ繋がっている、など)。npx -p @hatake-fw/api hatake validate が警告で言い、npx -p @hatake-fw/api hatake diagram --roles が図に重ね、explain が文で言う。3つとも同じ計算を使っているので、答えは食い違わない。
--page <id> を付けると、その画面の入口まで出る。直す場所は入口なので、これが無いと「食い違っている」と言われても動けない。
$ npx -p @hatake-fw/api hatake explain app.yaml --page price_master
## この画面を開ける人
・開けるのは … 誰も開けない(入口はあるが、権限が食い違っている)
・入口「単価」(customer_master から) … manager だけが通れる入口の権限を1つ直すと、遠くの画面が開けなくなることがある。定義を直したときは npx -p @hatake-fw/api hatake explain --diff --git main...HEAD app.yaml で読み返すと、その画面の「開ける人」が変わったことが出る。
CSV にも効く
type: export は「画面の列と行」から CSV を組むので、ロールで見えない列は CSV にも出ない。給与列を隠しているのに CSV には出てしまう、という事故が起きない作りになっている。
これは表示制御でしかない
重要な前提。roles が止めるのは画面に出すかどうかだけ。
- API を直接叩けばデータは取れる
- 隠した項目もレスポンスに含まれていれば通信内容には見えている
実際のアクセス制御はバックエンドで行う。 Framework は認証も認可も持たない(持たないと決めている)ので、ここは自分で守る必要がある。定義の roles は「間違って触らせない」ためのもので、「守る」ためのものではない。
サーバでも同じ定義で判断する
守るのはサーバだが、何を守るかは定義に書いてある。同じことをサーバで手書きすると、列の roles は見たのに入力欄の roles を忘れる、画面の権限を役割名で決め打ちする、が起きる(見本の3本とも起きていた)。サーバは定義から答えを引く(0.9.22。TS は @hatake-fw/api、Java は ServerAccess)。
import { acceptRecordIn, canOpenPageIn, canRunActionIn, visibleRecordIn } from "@hatake-fw/api";
if (!canOpenPageIn(definition, "employee_master", user.roles)) return res.status(403).end();
if (!canRunActionIn(definition, "employee_master", "delete", user.roles)) return res.status(403).end();
const rows = found.map((row) => visibleRecordIn(definition, "employee_master", row, user.roles));
const { accepted, dropped } = acceptRecordIn(definition, "employee_master", req.body, user.roles);| 口 | 答え |
|---|---|
canOpenPageIn | その画面を開けるか |
canRunActionIn | そのボタンを押せるか(画面に無いボタンは押せない。組み込みの新規登録は type: create を書いたときだけ、編集・削除は行に並べたときだけ) |
visibleRecordIn | 見せない項目を落としたレコード(列か入力欄のどこか一つでも隠していれば落とす) |
acceptRecordIn | 受け取ってよい項目だけ(入力欄にあって・見えて・読むだけでも計算でもない)と、落とした名前 |
「自分の拠点の行だけ」のような行の範囲は、ここには入らない。誰がどの行を見てよいかは認可そのもので、枠組みの外。
書いてあることは読まれないので、機械に試させるのが確実。
npx -p @hatake-fw/api hatake attack app.yaml --all-roles --accounts accounts.json --base http://localhost:8080/api 画面 hr / manager / 誰でもない人
employee_search hr=ok / manager=ok / 誰でもない人=要ログイン
salary_search hr=ok / manager=穴 / 誰でもない人=穴定義に出てくる役割ぜんぶと誰でもない人(資格なし)を並べて、見えないはずの口を叩く。穴が1つでもあれば終了コード 1 なので、API 側の CI に置ける。資格は役割ごとに要る(1つの資格で他の役割を判定すると、返ってきた 200 が穴なのか正しいのか区別できない)。
毎晩回す形にする
手で叩く道具は、忘れられたら手書きと同じ。CI に置くには2つ足りない。トークンは期限で落ちる(--token では続かない)し、出力は毎晩同じ(人は同じ表を読み続けられない)。
npx -p @hatake-fw/api hatake attack app.yaml --all-roles --login login.json --base "$BASE" \
--since last.json --save last.json --fail-on new--login は資格の取り方だけを書いたファイル(url / tokenAt / 役割ごとの roles)。合言葉は ${環境変数} で外から渡すので、ファイル自体はリポジトリに置ける。取ったトークンは報告にも --dry-run にも出さない(CI のログは残る)。--since は前回の結果と比べて変わった所だけ出し、--fail-on new は新しい分だけで落とす。
資格が切れた晩に静かに緑にならないことが、ここでいちばん大事な決めごと。役割を1つ叩けなくなると、その役割の穴は報告から消える=直ったように見える。なので前回叩けていた相手を今回叩いていなければ「叩いていないので分かりません」と言い、--fail-on new でも落とす。
同じロール判定の仕組みが Java / TypeScript にもあるので、同じ定義をサーバ側でも読んで、返す項目を落とすという使い方ができる。表示制御と実際の制御を、1つの定義から揃えられる。
現在のユーザのロールはどこから来るか
定義には書かない。アプリを起動するときに、その利用者のロールを Framework に渡す(Flutter なら HatakeScope(roles: {'admin'}))。ログイン処理も、ロールをどう決めるかも、Framework の外の話。
アプリが配る名前と、定義に書いた名前を突き合わせる
定義の中で綴りが揃っていても、アプリが配る名前と違えば誰にも見えない。しかもこれは画面を見ても気づけない(見えないのが正しい機能なので、「見えない」が正常と区別できない)。
そこで、アプリが配りうる役割の全部を宣言できるようにしてある。
HatakeScope(
knownRoles: const {'staff', 'manager'}, // このアプリが配りうる役割(語彙)
roles: session.roles, // いま見ている人の役割
...
)2つを分けるのが要点。roles はその時のログイン状態なので、突き合わせに使うと「staff でログインしている間は manager はアプリに無い」と言い出す=道具が嘘をつく。
宣言しておくと2つ効く。
| どうなるか | |
|---|---|
| 定義側の綴り違い | npx -p @hatake-fw/api hatake validate --registry が「役割 "managr" はアプリが配る役割の中にありません」と言う(近い名前も出す)。roles: に書いたものだけでなく、maxRows.byRole / batchSize.byRole に書いた役割も見る |
| アプリ側の綴り違い | 配った役割が語彙に無ければ、開発中に気づける(assert)。manager を manger で配っている、が画面を見ずに分かる |
一覧の作り方は登録済み一覧と同じ2通り(npx -p @hatake-fw/api hatake registry <path> でソースを読む/registrySnapshot(scope) で動いているアプリに聞く)。ソースを読む側は knownRoles: にその場で並んでいる名前(同じファイルの変数なら1回だけ辿る)を読み、読めなければ読めないと言う(空の一覧として扱うと「アプリは役割を1つも配っていない」という嘘になる)。
役割は消す相手ではないので、逆向きの棚卸し(refs --unused)には出さない。定義が使っていないからといって、アプリの認可から消す話にはならないので。
綴り違いは、並べると見つかる
5か所に散って書けるということは、綴りを間違えても何も起きないということでもある。manager と書くつもりで manaher と書いた列は、誰にも見えないまま出荷される(定義としては正しいので、検証も通る)。
書く前・直したあとに、定義に出てくる役割を数える。
npx -p @hatake-fw/api hatake explain app.yaml --roles販売管理(sales) — 出てくる役割 3
manager … 2 か所
・メニュー「原価」 … app.menu[1].roles
・ボタン「CSV 出力」(order_search) + admin … app.pages[0].actions[1].roles
manaher … 1 か所
・列「原価」(cost_search) … app.pages[1].table.columns[1].roles出てくる回数の多い順に並ぶので、1か所しか出てこない役割が下に落ちてくる。役割は普通あちこちで使われるので、1回しか出てこないものは疑ってよい。
役割ごとに「その役割だから開ける画面」も出る(誰でも開ける画面は数だけ添える)。ここが空なら、その役割は列や項目にしか効いていない。
hr … 7 か所に書いてある
・メニュー「給与」 … app.menu[2].roles
・列「給与」(employee_search) … app.pages[1].table.columns[3].roles
開ける画面 … salary_search(ほかに誰でも開ける画面 3 枚)「か所」は書いてある場所、「開ける画面」は入口を辿った結果。この2つは別の答えなので分けて出している。
一括で何件動かせるかも、役割から引ける
1回で動かせる件数(maxRows)と、何件ずつ渡すか(batchSize)はどちらも役割ごとに書ける。書く場所はボタンなので定義のあちこちに散り、「拠点の担当は1回に何件動かせるのか」を読むには全部のボタンを開くことになる。棚卸しに出しておけば、役割から引ける。
hr … 7 か所に書いてある
開ける画面 … salary_search(ほかに誰でも開ける画面 3 枚)
一括「昇給を一括承認」(employee_search) … 1回 50 件まで・区切りなし(1回で 50 件を渡す)
manager … 2 か所に書いてある
一括「昇給を一括承認」(employee_search) … 1回 5 件まで・区切りなし(1回で 5 件を渡す)同じボタンでも役割で件数が違う(hr は 50 件、manager は 5 件)ことが1枚で読める。上限は画面に出ている行の数で頭打ちになるので、maxRows: 500 と書いてあっても1ページ 200 件なら「1回 200 件まで」と出る=書いた数ではなく動く数を出している。
「区切りなし」は、選んだ行を1回でまとめてハンドラに渡す状態(進み具合も残り時間も出ず、途中で止められない)。batchSize を書くと枠組みが区切って回すので、そこが付いてくる。
maxRows.byRole に定義のどこにも無い役割を書いた場合だけは、validate が警告で言う(そこは「書いたのに効かない」=事実なので)。それ以外の綴り違いはこの一覧で人が見つけるしかない。ここに出るのは定義に書いてある名前だけで、アプリ側が実際にその役割を持っているかは見られない。
役割から引く(棚卸し)
5か所に散って書けるということは、役割から引くには定義を全部開くということでもある。人事異動のたびに聞かれるのは逆向き(「この役割で何ができるか」)なので、道具に聞く。
npx -p @hatake-fw/api hatake index app.yaml --role staff # その役割で開ける画面だけ
npx -p @hatake-fw/api hatake explain app.yaml --roles --matrix # 役割を横に並べた○×の表販売管理(sales_admin) — 役割ごとの見え方
見えるもの manager staff 誰でもない人
---------------------------------- ------- ----- ------------
画面「原価管理」 ○ × ×
列「粗利」(order_search) ○ × ×
ボタン「CSV出力」(order_search) ○ ○ ×誰でもない人(未ログイン)の列が必ず入る。 役割を持つ人だけを並べると、ログインしていない人に何が見えているかを誰も見ないままになる(デモで切り替えられるようにして最初に分かったのがそれ)。
並べているのは役割で絞られている物だけ。絞っていない物は全員に見えるので、並べると表が定義の大きさになって読まれない。
「画面」の行は入口を辿った結果。メニューやボタンの権限を辿って、最後に画面自身の roles を門として掛ける(メニューの行は同じことを言うので出さない)。
--role に定義のどこにも出てこない役割名を渡すとエラーにする。黙って0枚を返すと「その役割では何も開けない」と読めてしまうので。
explain --roles の役割ごとの行にも「見えるのは何件・見えないのは何件」が出る。「この役割で何ができるか」は、見えない側まで書かないと答えにならない。
アプリが配るのに、使っていない役割
--registry を渡すと逆の抜けも言う。
アプリが配るのに、定義が出し分けに使っていない役割 … viewer消す相手ではない。 アプリのコードで使っていることもあるし、出し分けの書き忘れのこともある。言えるのは「この役割で見え方が変わる所は1つもない」という事実だけ。逆向き(定義にしか無い役割)は validate --registry が警告で言う。
役割から引く(棚卸し)
5か所に散って書けるということは、役割から引くには定義を全部開くということでもある。人事異動のたびに聞かれるのは逆向き(「この役割で何ができるか」)なので、道具に聞く。
npx -p @hatake-fw/api hatake index app.yaml --role staff # その役割で開ける画面だけ
npx -p @hatake-fw/api hatake explain app.yaml --roles --matrix # 役割を横に並べた○×の表販売管理(sales_admin) — 役割ごとの見え方
見えるもの manager staff 誰でもない人
---------------------------------- ------- ----- ------------
画面「原価管理」 ○ × ×
列「粗利」(order_search) ○ × ×
ボタン「CSV出力」(order_search) ○ ○ ×誰でもない人(未ログイン)の列が必ず入る。 役割を持つ人だけを並べると、ログインしていない人に何が見えているかを誰も見ないままになる(デモで切り替えられるようにして最初に分かったのがそれ)。
並べているのは役割で絞られている物だけ。絞っていない物は全員に見えるので、並べると表が定義の大きさになって読まれない。
「画面」の行は入口を辿った結果。メニューやボタンの権限を辿って、最後に画面自身の roles を門として掛ける(メニューの行は同じことを言うので出さない)。
--role に定義のどこにも出てこない役割名を渡すとエラーにする。黙って0枚を返すと「その役割では何も開けない」と読めてしまうので。
explain --roles の役割ごとの行にも「見えるのは何件・見えないのは何件」が出る。「この役割で何ができるか」は、見えない側まで書かないと答えにならない。
アプリが配るのに、使っていない役割
--registry を渡すと逆の抜けも言う。
アプリが配るのに、定義が出し分けに使っていない役割 … viewer消す相手ではない。 アプリのコードで使っていることもあるし、出し分けの書き忘れのこともある。言えるのは「この役割で見え方が変わる所は1つもない」という事実だけ。逆向き(定義にしか無い役割)は validate --registry が警告で言う。
配りうる役割を、定義に書いておく
roles に書くのは自分で決めた文字列なので、綴りを間違えても画面は出る(その列やボタンが誰にも見えなくなるだけ)。画面を見ても「そういう仕様」に見えるので、いちばん気づきにくい。
app.roles に配りうる役割の全部を並べておくと、そこは機械が言う。
app:
id: hr_admin
title: 人事管理
roles: [hr, manager, executive]$ npx -p @hatake-fw/api hatake validate app.yaml
警告 app.pages[1].table.columns[3].roles: 役割 "mgr" はアプリが配る役割の中にありません。
その役割で出し分けている所は**誰にも見えません**(列もボタンも出ません)。
→ もしかして "manager" ですか。並べても何も与えない。 出し分けは各所の roles、実際の遮断はサーバ。ここは語彙の宣言で、権限の設定ではない。
書かなければ今までどおりで、この検査はアプリ側の一覧(HatakeScope(knownRoles:)/validate --registry)が要る。つまり app.roles の値打ちは定義1枚で閉じること ── AI に定義だけ渡したときに、綴りを自分で確かめられる。
逆向き(宣言したのに、どこにも出し分けに使っていない役割)も explain --roles が言う。これも消す相手ではなく、「この役割で見え方が変わる所は1つもない」という事実。
デモで切り替えて見る
権限は見えないことが正しい機能なので、書いてあるだけでは伝わらない。デモの右下に「見ている人」の札があり、そこで切り替えると同じ画面が変わる。
| 見ている人 | 受注照会で何が変わるか |
|---|---|
担当(staff) | 粗利の列は出ない・却下のボタンも出ない・原価管理はメニューに無い |
承認者(staff + manager) | 粗利の列と却下が出る・原価管理の入口が出る・一括承認は 50 件まで(担当は 20 件) |
| 誰でもない(未ログイン) | CSV出力が出ない(持ち出しはログインした人だけ) |
切り替えても開いている画面はそのままなので、同じ一覧で「何が消えたか」を見比べられる。役割はアプリが配るもの(ログイン状態)なので、この札は定義の側ではなくデモアプリ自身の作り。
承認者が担当の役割も持っている({staff, manager})のは、役割が足し算だから。「承認者だから担当の画面が見えない」ことにはならない。
ロール名の付け方
roles に書くのは自分で決めた文字列で、Framework は意味を知らない。業務の役割(hr manager approver)で名付けると読める定義になる。画面や操作の名前(can_export)で付けると、権限体系が画面数に比例して増えていくので避けたほうがいい。
書けるキー
| キー | 書く場所 | 型 | 必須 | 既定値 | 有効なページ種別 | 説明 |
|---|---|---|---|---|---|---|
roles | action | array of string | 任意 | — | crud dashboard detail form master report search wizard | Roles allowed to see this item/action. Empty or absent = everyone. UI-level display gating only, not access enforcement. |
roles | app | array of string | 任意 | — | すべて | Every role this app may hand out (the vocabulary, not a gate). Writing it lets the tools check role names with the definition alone: a role used on a menu item, action, column, field, filter or card that is not listed here is a typo, and anything gated on it is visible to nobody. Absent = the vocabulary is unknown, so that check needs the application registry (HatakeScope(knownRoles:)) instead. Listing a role here grants nothing: display gating is roles on each item, and enforcement is the server. |
roles | column | array of string | 任意 | — | crud dashboard detail form master report search wizard | Roles allowed to see this item/action. Empty or absent = everyone. UI-level display gating only, not access enforcement. |
roles | crudPage | array of string | 任意 | — | crud | Who may open this page (empty or absent = everyone). The menu hides entries that lead here for anyone else, the page itself refuses to render for them, and servers read the same list (TS canOpenPageIn / Java ServerAccess.canOpenPage). |
roles | dashboardItem | array of string | 任意 | — | dashboard | Roles allowed to see this card. Empty = everyone. |
roles | dashboardPage | array of string | 任意 | — | dashboard | Who may open this page (empty or absent = everyone). The menu hides entries that lead here for anyone else, the page itself refuses to render for them, and servers read the same list (TS canOpenPageIn / Java ServerAccess.canOpenPage). |
roles | detailPage | array of string | 任意 | — | detail | Who may open this page (empty or absent = everyone). The menu hides entries that lead here for anyone else, the page itself refuses to render for them, and servers read the same list (TS canOpenPageIn / Java ServerAccess.canOpenPage). |
roles | field | array of string | 任意 | — | crud dashboard detail form master report search wizard | Roles allowed to see this item/action. Empty or absent = everyone. UI-level display gating only, not access enforcement. |
roles | formPage | array of string | 任意 | — | form | Who may open this page (empty or absent = everyone). The menu hides entries that lead here for anyone else, the page itself refuses to render for them, and servers read the same list (TS canOpenPageIn / Java ServerAccess.canOpenPage). |
roles | masterPage | array of string | 任意 | — | master | Who may open this page (empty or absent = everyone). The menu hides entries that lead here for anyone else, the page itself refuses to render for them, and servers read the same list (TS canOpenPageIn / Java ServerAccess.canOpenPage). |
roles | menuItem | array of string | 任意 | — | すべて | Roles allowed to see this item/action. Empty or absent = everyone. UI-level display gating only, not access enforcement. |
roles | reportPage | array of string | 任意 | — | report | Who may open this page (empty or absent = everyone). The menu hides entries that lead here for anyone else, the page itself refuses to render for them, and servers read the same list (TS canOpenPageIn / Java ServerAccess.canOpenPage). |
roles | searchPage | array of string | 任意 | — | search | Who may open this page (empty or absent = everyone). The menu hides entries that lead here for anyone else, the page itself refuses to render for them, and servers read the same list (TS canOpenPageIn / Java ServerAccess.canOpenPage). |
roles | wizardPage | array of string | 任意 | — | wizard | Who may open this page (empty or absent = everyone). The menu hides entries that lead here for anyone else, the page itself refuses to render for them, and servers read the same list (TS canOpenPageIn / Java ServerAccess.canOpenPage). |
この表は spec/reference.json から生成している(JSON Schema が正)。手元では npx -p @hatake-fw/api hatake reference <キー名> で同じものが引ける。
近い例
例は丸ごと写して直すのが一番速い。以下は CI で検証済み(そのまま動く形)。
| ファイル | 種別 | 画面 | どういうときに使うか |
|---|---|---|---|
roles_app.yaml | app | 人事管理 | 見せる相手を役割で変えたい(画面ごと隠す・列や項目だけ隠す・押せる人を絞る) |