🔐

ReactでCaslを使用して認可処理を実現する。

2024/01/29に公開

caslとは

公式サイト

CASL (pronounced /ˈkæsəl/, like castle) is an isomorphic authorization JavaScript library which restricts what resources a given client is allowed to access. It's designed to be incrementally adoptable and can easily scale between a simple claim based and fully featured subject and attribute based authorization. It makes it easy to manage and share permissions across UI components, API services, and database queries.

アプリケーション内で認可の設定を一括でまとめてくれる便利なライブラリ

できること

4つのパラメータによって実行できる能力が決まる。2~3は、オプショナルで設定できる。

  1. ユーザーアクション
    ユーザーができることを定義する。
    (read, update, create, delete...)
  2. サブジェクト
    サブジェクトに対して、ユーザーアクションを確認するための型
    (User, Article...)
  3. フィールド
    一致したフィールドのみにユーザーアクションを設定することができる
  4. 条件
    ユーザーアクションを制限する時に使用する

実際に使用してみる

状況

  • ユーザー登録をして使用するブログアプリケーション

  • 登録するとUserデータベースに登録される

  • Userには2種類のタイプがある (adminとuser)

    • adminユーザーの場合すべての権限が与えられる。
  • すべてのユーザーは記事を作成することができる

  • 記事はArticleデータベースに登録される

  • ユーザーはすべての記事に対して、それぞれ異なった権限(role)を持つ

    • editor(編集者)、reader(閲覧者)、権限なし(null)
  • 記事に対してユーザーは、それぞれの権限に応じて操作が可能となる
      - create(作成)→ 誰でも

    • read(閲覧) → editor、reader
    • update(編集)→ editor
    • delete(削除)→ editor

設定

  1. ユーザーアクション
    create、read、update、delete
    を設定する
  2. サブジェクト
    Articleに権限を設定したいのでArticleを設定
  3. フィールド
    Article内のroleプロパティを確認し、ユーザー権限を確認するため、roleを設定
  4. 条件
    role がeditorかreaderかをチェックする

また、userのtypeによって権限を切り分ける必要がある

使い方

こんな感じで設定して使えるらしい。

import { defineAbility } from '@casl/ability';

export default defineAbility((can, cannot) => {
  can('read', 'Article');
  can('update', 'Article');
  can('delete', 'Article');
});

通常の使い方より少しカスタマイズしたいのでここを見ていく。https://casl.js.org/v4/en/advanced/customize-ability#extend-conditions-with-custom-operators

casl.tsファイル

import {
  AbilityBuilder,
  MongoAbility,
  createMongoAbility,
} from '@casl/ability';
import { createContextualCan } from '@casl/react';
import { createContext } from 'react';
type ArticleType = Article & {
  'role': 'editor' | 'reader' | null
};

export function defineAbilityFor(user?: User) {
  const { can, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
  if (user?.type === 'user') {
    can('create', 'Article');
    can<ArticleType>('read', 'Article', {
      'role': { $in: ['editor', 'reader'] },
    });
    can<ArticleType>('update', 'Article', {
      'role': { $in: ['editor'] },
    });
    can<ArticleType>('delete', 'Article', {
      'role': { $in: ['editor'] },
    });
  } else if (user?.type === 'admin') {
    can('create', 'Article');
    can('read', 'Article');
    can('update', 'Article');
    can('delete', 'Article');
  }

  return build();
}

export const AbilityContext = createContext({} as AppAbility);
export const Can = createContextualCan(AbilityContext.Consumer);

まず必要なものをもろもろimportする。
とりあえずdefineAbilityForの中で設定する。
const { can, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
で、設定に必要なものを定義している。

  1. userのtypeごとに権限を切り分ける
  2. canのあとの<ArticleType>で受け取るタイプを決めている。
  3. サブジェクトはArticle
  4. 'role': { $in: ['editor'] } はArticleの中のroleがeditorの場合のみ、指定された権限を与える、という処理。
  5. adminだったら無条件ですべての権限が付与される
...
 <AbilityContext.Provider value={defineAbilityFor(user)}>
   <Outlet />
 </AbilityContext.Provider>

最上位コンポーネントとで呼び出して、常にuser情報を与えておく

基本的に2種類の使い方ができる。 casl.tsのこの部分!

export const AbilityContext = createContext({} as AppAbility);
export const Can = createContextualCan(AbilityContext.Consumer);

1. 権限があるかどうかを知りたい時

import { useAbility } from '@casl/react';
import { subject } from '@casl/ability';
import { AbilityContext } from '@utils/abac';
import { FC } from 'react';


export const Article: FC = (article: Article) => {
  const ability = useAbility(AbilityContext);

  const canEdit: boolean =  ability.cannot('update', subject('Article', {...article }))

  return {
  .... 

2. 権限の結果をコンポーネントに反映したい時(ここ

権限がある場合のみ、コンポーネントを表示する

...
   <Can I="update" this={article}>
     <Button>
       編集
     </Button>
  </Can>

感想

コンポーネント毎で、article.type === 'editor' などを使って出しわけ処理をしていたが、caslを使えば、一気に設定できて楽!
一元管理できるのもいいね💕

Discussion