権限で出し分ける
ロールに応じてボタン・列・項目・メニューを出す/出さない。
ロールで出し分けたい要素に roles を書く。書いたロールのどれかを持っている人にだけ見える。
- { field: salary, label: 給与, type: number, roles: [hr, manager] }
actions:
- { id: export, type: plugin, plugin: csvExport, label: CSV出力, roles: [admin] }roles を書かなければ全員に見える。空配列も同じ扱い。
5か所に書ける
| 書ける場所 | 効果 |
|---|---|
field | その入力項目・表示項目が見えない |
column | 一覧のその列が出ない |
action | そのボタンが出ない(行のボタンも) |
dashboardItem | そのカードが出ない |
menuItem | メニューのその項目・グループが出ない |
つまり「画面に出るもの」には概ね付けられる。ページ自体に付けることはできないので、画面ごと見せたくないならメニュー項目に付ける。
「この画面は誰が開けるか」は辿らないと分からない
ページ自体に roles を書けないので、画面を開ける人は入口から辿るしかない。メニュー項目が admin だけなら、その先の画面も admin だけ。そこにあるボタンが誰でも押せる形で書いてあっても、来られるのが admin だけなら開けるのは admin だけ。
1枚の定義を読んでも出ない値なので、道具に聞く。
$ npx hatake explain app.yaml
## 画面を開ける人
・受注照会(order_search) … 誰でも開ける
・顧客マスタ(customer_master) … admin だけ
・単価マスタ(price_master) … 誰も開けない(入口の権限が食い違っている)最後の行のような誰も開けない画面は、定義としては通るし、画面を見ても気づけない(staff の画面に admin 限定のボタンでだけ繋がっている、など)。npx hatake validate が警告で言い、npx hatake diagram --roles が図に重ね、explain が文で言う。3つとも同じ計算を使っているので、答えは食い違わない。
--page <id> を付けると、その画面の入口まで出る。直す場所は入口なので、これが無いと「食い違っている」と言われても動けない。
$ npx hatake explain app.yaml --page price_master
## この画面を開ける人
・開けるのは … 誰も開けない(入口はあるが、権限が食い違っている)
・入口「単価」(customer_master から) … manager だけが通れる入口の権限を1つ直すと、遠くの画面が開けなくなることがある。定義を直したときは npx hatake explain --diff --git main...HEAD app.yaml で読み返すと、その画面の「開ける人」が変わったことが出る。
CSV にも効く
type: export は「画面の列と行」から CSV を組むので、ロールで見えない列は CSV にも出ない。給与列を隠しているのに CSV には出てしまう、という事故が起きない作りになっている。
これは表示制御でしかない
重要な前提。roles が止めるのは画面に出すかどうかだけ。
- API を直接叩けばデータは取れる
- 隠した項目もレスポンスに含まれていれば通信内容には見えている
実際のアクセス制御はバックエンドで行う。 Framework は認証も認可も持たない(持たないと決めている)ので、ここは自分で守る必要がある。定義の roles は「間違って触らせない」ためのもので、「守る」ためのものではない。
書いてあることは読まれないので、機械に試させるのが確実。
npx 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 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 hatake validate --registry が「役割 "managr" はアプリが配る役割の中にありません」と言う(近い名前も出す)。roles: に書いたものだけでなく、maxRows.byRole / batchSize.byRole に書いた役割も見る |
| アプリ側の綴り違い | 配った役割が語彙に無ければ、開発中に気づける(assert)。manager を manger で配っている、が画面を見ずに分かる |
一覧の作り方は登録済み一覧と同じ2通り(npx hatake registry <path> でソースを読む/registrySnapshot(scope) で動いているアプリに聞く)。ソースを読む側は knownRoles: にその場で並んでいる名前(同じファイルの変数なら1回だけ辿る)を読み、読めなければ読めないと言う(空の一覧として扱うと「アプリは役割を1つも配っていない」という嘘になる)。
役割は消す相手ではないので、逆向きの棚卸し(refs --unused)には出さない。定義が使っていないからといって、アプリの認可から消す話にはならないので。
綴り違いは、並べると見つかる
5か所に散って書けるということは、綴りを間違えても何も起きないということでもある。manager と書くつもりで manaher と書いた列は、誰にも見えないまま出荷される(定義としては正しいので、検証も通る)。
書く前・直したあとに、定義に出てくる役割を数える。
npx 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 が警告で言う(そこは「書いたのに効かない」=事実なので)。それ以外の綴り違いはこの一覧で人が見つけるしかない。ここに出るのは定義に書いてある名前だけで、アプリ側が実際にその役割を持っているかは見られない。
ロール名の付け方
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 | 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 | dashboardItem | array of string | 任意 | — | dashboard | Roles allowed to see this card. Empty = everyone. |
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 | menuItem | array of string | 任意 | — | すべて | Roles allowed to see this item/action. Empty or absent = everyone. UI-level display gating only, not access enforcement. |
この表は spec/reference.json から生成している(JSON Schema が正)。手元では npx hatake reference <キー名> で同じものが引ける。
近い例
例は丸ごと写して直すのが一番速い。以下は CI で検証済み(そのまま動く形)。
| ファイル | 種別 | 画面 | どういうときに使うか |
|---|---|---|---|
roles_app.yaml | app | 人事管理 | 見せる相手を役割で変えたい(画面ごと隠す・列や項目だけ隠す・押せる人を絞る) |