🤧

ロールによるメソッドアクセス制御のためのTypescriptデコレータパターン

2023/05/29に公開

目次

  • 背景
  • デコレータを使用する理由
  • デコレータとは
  • デコレータの実装

背景

アプリケーション内であるメソッド(ユーザの削除や閲覧)を実行する時にユーザのロールによって,メソッドの実行可否を分けたい.
この時にそれぞれのメソッド内にif文で

if (!user.role == 'admin'){
        throw new Error(`${user.role}ではこの操作は行えません.`);
}

のように条件分岐を入れる書き方もできるが実行権限の範囲が変わった時にコードを変更する必要があったり, それぞれのメソッドにつく権限の範囲がメソッドの内部を見ないとわからない.
以上の理由からロールによる権限の仕組みは外部で行い,一目でわかるようにし,変更も容易にしたい.

デコレータを使用する理由

ここでTypescriptで使用されるDecoratorという機能を使用することで,以下のように

class UserController {
    @Roles('admin')
    deleteUser(userId: String) {
        console.log(`${userId} was deleted`);
        // code to delete user
    }@Roles('admin', 'manager')
    showUsers() {
        console.log('showing users');
        // code to show users
    }
}

ロールによってそのメソッドの実行権限があるかどうかが一目瞭然に記述でき,その切り分けはそれぞれのメソッド内で行われないような実装にすることができる.
このようなメリットがあるので今回はデコレータで実装を実装することで,メソッドの実行権限の切り分けを行う.

デコレータとは

デコレータとは,Typescript, Javascriptの機能である.
これはデコレータパターンを実装するために使われるものである.

デコレータパターンとは構造に関するデザインパターンの一つ.
このパターンはオブジェクトをラップし, 新しい機能を追加するためのラッパー(デコレータ)を作成することで既存のオブジェクトを変更することなく機能を拡張したり,分離された機能をもさせることができる.
これがメインのデコレータパターンのアイディア.

デコレータを使用せずに,自力で実際にデコレータパターンを実装する時にTypescriptではオブジェクト毎にextendsで継承する必要がある.

よって,簡易的に実装するためのインターフェイスとして,デコレータという機能が提供されている.

デコレータにはいくつか種類があり,それぞれの宣言の上に記述することで適用される.今回はメソッドデコレータを使用する.
クラスデコレータ: クラス宣言の直前に適用される.
メソッドデコレータ: メソッド宣言の直前に適用される.
プロパティデコレータ: プロパティ宣言の直前に適用される.
パラメータデコレータ: メソッドのパラメータの直前に適用される.

デコレータの実装


まずデコレータを作成する.

const Roles = (...allowedRoles: any) => {
    return (target: any, name: string, descriptor: PropertyDescriptor) => {
        descriptor.value.allowedRoles = allowedRoles;
        return descriptor;
    };
}

ここでは, ...allowedRolesで任意の数のメソッドの実行が許可されるrolesを受け取る.
descriptor.valueに,適用したいメソッドのオブジェクトが入っている.ここに対して,プロパティとして,allowedRolesを追加で加える.そしてそのまま,メソッドを返すというデコレータRolesを定義できた.

次に実際にデコレータにadminを入れてdeleteUserメソッドの上に記述することで,deleteUserにはプロパティとしてallowedRolesにadminを持つメソッドとなった.

class UserController {
    @Roles('admin')
    deleteUser(userId: String) {
        console.log(`${userId} was deleted`);
        // code to delete user
    }
}

次に,メソッドを受け取り,そのメソッドに対して,roleによってガードするための関数を実装する.

const guard = (userRole: String, methodName: String, controller: UserController) => {
    const method = (controller as any)[methodName as any] as any;
    const allowedRoles: String[] = method.allowedRoles;if (!allowedRoles.includes(userRole)) {
        throw new Error(`User with role ${userRole} is not allowed to access ${methodName}`);
    }
    return method;
}

userRoleにはクライアントのroleが入り,methodNameには呼び出したいメソッド名,controllerにはUserControllerのインスタンスが入る.ここで,controllerインスタンスの持つmethodNameという名前を持つメソッド(今回はdeleteUser)オブジェクトをmethod変数にいれる.このメソッドは先ほどのデコレータ内で代入されたallowedRolesをプロパティとして持っている.このallowedRolesの中に,クライアントのuserRoleが含まれている場合そのままメソッドを返す.含まれていない場合はthrow new Errorによってエラーを吐き出す.

ここからは実際にクライアントが使用するコードである.

const controller = new UserController();

// an example user
const user = { role: 'manager', id: '1' };try {
    const guardedDeleteUser = guard(user.role, 'deleteUser', controller);
    guardedDeleteUser(user.id);
} catch (error: any) {
    console.error(error.message);
}

以上のようにUserControllerをインスタンスかさせ,userオブジェクトを仮に作成.guard関数に実行したいメソッド名とともに,渡すことで,roleによって実行可否を分ける機能が追加されたguardedDeleteUserを作成.この関数を実際に呼び出すことで,正しいroleを持つuserのみが実行できる.
おわり.

Discussion