
CASLでの権限管理で気になったところを追加調査してみた
先日投稿した CASL を用いた権限管理について、cannot
に関してや、権限の優先順位など実際に使っていくにあたっての気になりどころを調査してみました。
データ構造など 前回の記事 のものを流用・拡張したものを利用します。
cannot 関数
ALLOW 権限を付与する can
関数に対し、DENY 権限を付与する cannot
関数が存在します。
AbilityBuilder を用いて Ability にルールを追加する形で付与します。
const buildAbility = (userId: number) => {
// ここの cannot を使う
const { can, cannot, build } = new AbilityBuilder<AppAbility>(
createPrismaAbility
);
// 投稿は削除不可とする権限の場合
cannot('delete', 'Post');
...省略
return build();
};
can
と cannot
の権限範囲が重複した場合の優先度ですが、ALLOW と DENY の関係と同じく cannot
の方が優先されます。
例えば以下のような範囲の重複した権限が共存していた場合、cannot
の方が優先度が高いため、 cannot
の条件に抵触している場合はアクセス不可と判定されます。
広い範囲でアクセスを許可するリソースに対し、特定の条件を満たすリソースはアクセス不可としたい場合などに利用したいですね。
// 全ての公開済みの投稿を閲覧できる
can('read', 'Post', { published: true });
// 自身の非公開の投稿を閲覧できる
can('read', 'Post', { published: false, authorId: userId });
// 削除済みの投稿は閲覧できない
cannot('read', 'Post', { isDelete: true });
...
// Abilityをビルド
const ability = buildAbility(user.id);
// 自身が作成した公開済み、削除済みの投稿への参照権限があるかチェック
const canAccess = ability.can(
'read',
subject('Post', {
authorId: user.id,
published: true,
isDelete: true,
}) as AppSubjects
);
// isDeleteがtrueの場合は参照不可とされているため、canAccessはfalse
console.log('canAccess', canAccess);
ルール作成時の condition の有無と権限判定時の condition の有無
Ability のルールを作成する際の条件の有無と、権限を判定する際の条件の有無で注意すべきことがあります。
例えば以下のような権限があったとします。
// 自身の投稿を削除できる
can('delete', 'Post', { authorId: userId });
こちらに対し投稿の全データの削除権限をチェックしようとした場合、直感的には以下の記述としたくなり、期待値は false となります。
ですが以下の canAccess は true です。
// 投稿に対し無条件の削除権限があるか確認したい(期待値はfalse)
// しかしここでのcanAccessはtrue
const canAccess = ability.can('delete', 'Post');
Ability.can
での条件を未指定とした場合、CASL では Action、Subject に対する全権限があるかではなく、指定した Action、Subject を利用したルールが存在するかを判定しているようです。
理由は こちら に記載されているのですが、 create
のような特定のオブジェクトに依存しない権限の確認を迅速に行うための措置のようです。
では全権限があるかどうかのチェックはどのように行えば良いかですが、以下のようにすると期待通りの動作となりました。
権限は上のコードと同じのため割愛します。
// canAccessがfalseになる
const canAccess = ability.can('delete', subject('Post', {}) as AppSubjects);
特定の Action に対し、無条件で権限を付与したいケースはどうでしょうか。
ルールの記述は直感的には以下の記述になると思います。
// 全投稿を削除できるようにしたい
can('delete', 'Post');
これに対し、特定のオブジェクトへの削除権限を確認してみます。
こちらは期待通り true となりました。
// 他人の投稿が削除可能か確認したい(期待値はtrue)
// canAccessはtrue
const canAccess = ability.can(
'delete',
subject('Post', {
authorId: otherUser.id,
}) as AppSubjects
);
上記で紹介した条件を問わず権限があるかどうかのチェックも実施しましたが、こちらも期待通り true となります。
// 全投稿が削除可能か確認したい(期待値はtrue)
// canAccessはtrue
const canAccess = ability.can('delete', subject('Post', {}) as AppSubjects);
条件問わず権限を付与する場合は Action、Subject のみを指定する方法で良さそうです。
まとめ
今回は短かめです。
CASL を使い始めて「こうしたい」や「ん?」と思った箇所があったので記事にしてみました。
直感と実態が異なっている部分があるため、どのような思想で設計されているかきちんと把握して使うよう心がけたいですね。