認可

認可

@adonisjs/bouncerパッケージを使用して、AdonisJSアプリケーションで認可チェックを行うことができます。Bouncerは、アビリティポリシーとしての認可チェックを書くためのJavaScriptファーストのAPIを提供します。

アビリティとポリシーの目的は、アクションの認可ロジックを1つの場所に抽象化し、コードベース全体で再利用することです。

  • アビリティは関数として定義され、アプリケーションに少なくかつシンプルな認可チェックがある場合に適しています。

  • ポリシーはクラスとして定義され、アプリケーションの各リソースごとに1つのポリシーを作成する必要があります。ポリシーはまた、依存性の自動注入の恩恵を受けることもできます。

BouncerはRBACやACLの実装ではありません。代わりに、AdonisJSアプリケーションでアクションを認可するための細かい制御を持つ低レベルのAPIを提供しています。

インストール

次のコマンドを使用してパッケージをインストールして設定します:

node ace add @adonisjs/bouncer
  1. 検出されたパッケージマネージャを使用して@adonisjs/bouncerパッケージをインストールします。

  2. 次のサービスプロバイダとコマンドをadonisrc.tsファイル内に登録します。

    {
    commands: [
    // ...other commands
    () => import('@adonisjs/bouncer/commands')
    ],
    providers: [
    // ...other providers
    () => import('@adonisjs/bouncer/bouncer_provider')
    ]
    }
  3. app/abilities/main.tsファイルを作成し、アビリティを定義してエクスポートします。

  4. app/policies/main.tsファイルを作成し、すべてのポリシーをコレクションとしてエクスポートします。

  5. middlewareディレクトリ内にinitialize_bouncer_middlewareを作成します。

  6. start/kernel.tsファイル内に次のミドルウェアを登録します。

    router.use([
    () => import('#middleware/initialize_bouncer_middleware')
    ])

視覚的な学習者の方へ - Adocastsの友達からのAdonisJS Bouncer無料スクリーンキャストシリーズをチェックしてみてください。

Bouncerの初期化ミドルウェア

セットアップ中に、アプリケーション内に#middleware/initialize_bouncer_middlewareミドルウェアを作成して登録します。初期化ミドルウェアは、現在の認証済みユーザーのためにBouncerクラスのインスタンスを作成し、リクエストの残りの部分でctx.bouncerプロパティを介して共有します。

また、ctx.view.shareメソッドを使用してEdgeテンプレートとも同じBouncerインスタンスを共有します。アプリケーション内でEdgeを使用していない場合は、ミドルウェアから次のコードを削除しても構いません。

初期セットアップ時に作成されるファイルを含め、アプリケーションのソースコードはすべてあなたのものです。そのため、それらを変更してアプリケーション環境に合わせて動作させることに躊躇しないでください。

async handle(ctx: HttpContext, next: NextFn) {
ctx.bouncer = new Bouncer(
() => ctx.auth.user || null,
abilities,
policies
).setContainerResolver(ctx.containerResolver)
/**
* Edgeを使用していない場合は削除してください
*/
if ('view' in ctx) {
ctx.view.share(ctx.bouncer.edgeHelpers)
}
return next()
}

アビリティの定義

アビリティは通常./app/abilities/main.tsファイル内に記述されるJavaScript関数です。このファイルから複数のアビリティをエクスポートできます。

次の例では、Bouncer.abilityメソッドを使用してeditPostというアビリティを定義しています。実装コールバックは、ユーザーを認可するためにtrueを返し、アクセスを拒否するためにfalseを返す必要があります。

アビリティは常に認可チェックに必要な追加のパラメーターに続いて、最初のパラメーターとしてUserを受け入れるべきです。

app/abilities/main.ts
import User from '#models/user'
import Post from '#models/post'
import { Bouncer } from '@adonisjs/bouncer'
export const editPost = Bouncer.ability((user: User, post: Post) => {
return user.id === post.userId
})

認可の実行

アビリティを定義したら、ctx.bouncer.allowsメソッドを使用して認可チェックを実行できます。

Bouncerは、アビリティコールバックに現在ログインしているユーザーを自動的に最初のパラメーターとして渡し、残りのパラメーターは手動で指定する必要があります。

import Post from '#models/post'
import { editPost } from '#abilities/main'
import router from '@adonisjs/core/services/router'
router.put('posts/:id', async ({ bouncer, params, response }) => {
/**
* 認可チェックを行うために、IDで投稿を検索します。
*/
const post = await Post.findOrFail(params.id)
/**
* アビリティを使用して、ログインしているユーザーが
* アクションを実行できるかどうかを確認します。
*/
if (await bouncer.allows(editPost, post)) {
return '投稿を編集できます。'
}
return response.forbidden('投稿を編集することはできません。')
})

bouncer.allowsメソッドの反対はbouncer.deniesメソッドです。if notステートメントを書く代わりに、このメソッドを使用することもできます。

if (await bouncer.denies(editPost, post)) {
response.abort('投稿を編集することはできません。', 403)
}

ゲストユーザーの許可

デフォルトでは、Bouncerはログインしていないユーザーに対して認可チェックを拒否し、アビリティコールバックを呼び出しません。

ただし、ゲストユーザーでも動作する特定のアビリティを定義する場合があります。たとえば、ゲストには公開された投稿を表示することを許可し、投稿の作成者には下書きも表示することを許可します。

allowGuestオプションを使用して、ゲストユーザーを許可するアビリティを定義できます。この場合、オプションは最初のパラメーターとして定義され、コールバックは2番目のパラメーターとして定義されます。

export const viewPost = Bouncer.ability(
{ allowGuest: true },
(user: User | null, post: Post) => {
/**
* 公開された投稿には誰でもアクセスできるようにします。
*/
if (post.isPublished) {
return true
}
/**
* ゲストは非公開の投稿を表示できません。
*/
if (!user) {
return false
}
/**
* 投稿の作成者は非公開の投稿も表示できます。
*/
return user.id === post.userId
}
)

ログインしているユーザー以外のユーザーの認可

ログインしているユーザー以外のユーザーを認可する場合は、Bouncerコンストラクタを使用して指定されたユーザーのために新しいBouncerインスタンスを作成できます。

import User from '#models/user'
import { Bouncer } from '@adonisjs/bouncer'
const user = await User.findOrFail(1)
const bouncer = new Bouncer(user)
if (await bouncer.allows(editPost, post)) {
}

ポリシーの定義

ポリシーは、認可チェックをクラスとして組織化するための抽象化レイヤーを提供します。通常、リソースごとに1つのポリシーを作成することをオススメします。たとえば、アプリケーションにPostモデルがある場合、投稿の作成や更新などのアクションを認可するためにPostPolicyクラスを作成する必要があります。

ポリシーは./app/policiesディレクトリ内に格納され、各ファイルが単一のポリシーを表します。次のコマンドを実行して新しいポリシーを作成できます。

参照:ポリシーの作成コマンド

node ace make:policy post

ポリシークラスはBasePolicyクラスを拡張し、実行したい認可チェックのためのメソッドを実装できます。次の例では、createeditdeleteの投稿に対する認可チェックを定義しています。

app/policies/post_policy.ts
import User from '#models/user'
import Post from '#models/post'
import { BasePolicy } from '@adonisjs/bouncer'
import { AuthorizerResponse } from '@adonisjs/bouncer/types'
export default class PostPolicy extends BasePolicy {
/**
* すべてのログインユーザーは投稿を作成できます。
*/
create(user: User): AuthorizerResponse {
return true
}
/**
* 投稿の作成者のみが投稿を編集できます。
*/
edit(user: User, post: Post): AuthorizerResponse {
return user.id === post.userId
}
/**
* 投稿の作成者のみが投稿を削除できます。
*/
delete(user: User, post: Post): AuthorizerResponse {
return user.id === post.userId
}
}

認可の実行

ポリシーを作成したら、bouncer.withメソッドを使用して認可に使用するポリシーを指定し、bouncer.allowsまたはbouncer.deniesメソッドをチェーンして認可チェックを実行できます。

bouncer.withメソッドの後にチェーンされたallowsメソッドとdeniesメソッドは型安全であり、ポリシークラスで定義したメソッドに基づいて補完リストが表示されます。

import Post from '#models/post'
import PostPolicy from '#policies/post_policy'
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async create({ bouncer, response }: HttpContext) {
if (await bouncer.with(PostPolicy).denies('create')) {
return response.forbidden('投稿を作成することはできません。')
}
//コントローラのロジックを続行します
}
async edit({ bouncer, params, response }: HttpContext) {
const post = await Post.findOrFail(params.id)
if (await bouncer.with(PostPolicy).denies('edit', post)) {
return response.forbidden('投稿を編集することはできません。')
}
//コントローラのロジックを続行します
}
async delete({ bouncer, params, response }: HttpContext) {
const post = await Post.findOrFail(params.id)
if (await bouncer.with(PostPolicy).denies('delete', post)) {
return response.forbidden('投稿を削除することはできません。')
}
//コントローラのロジックを続行します
}
}

ゲストユーザーの許可

アビリティと同様に、ポリシーも@allowGuestデコレータを使用してゲストユーザーのための認可チェックを定義できます。

例:

import User from '#models/user'
import Post from '#models/post'
import { BasePolicy, allowGuest } from '@adonisjs/bouncer'
import { AuthorizerResponse } from '@adonisjs/bouncer/types'
export default class PostPolicy extends BasePolicy {
@allowGuest()
view(user: User | null, post: Post): AuthorizerResponse {
/**
* 公開された投稿には誰でもアクセスできるようにします。
*/
if (post.isPublished) {
return true
}
/**
* ゲストは非公開の投稿を表示できません。
*/
if (!user) {
return false
}
/**
* 投稿の作成者は非公開の投稿も表示できます。
*/
return user.id === post.userId
}
}

ポリシーフック

beforeメソッドとafterメソッドをポリシークラスに定義することで、認可チェックの周りでアクションを実行できます。一般的な使用例は、特定のユーザーに常にアクセスを許可または拒否することです。

beforeメソッドとafterメソッドは、ログインしているユーザーの有無に関係なく常に呼び出されます。そのため、userの値がnullになる場合を扱う必要があります。

beforeメソッドの応答は次のように解釈されます。

  • trueの値は成功した認可と見なされ、アクションメソッドは呼び出されません。
  • falseの値はアクセスが拒否されたと見なされ、アクションメソッドは呼び出されません。
  • undefinedの戻り値の場合、バウンサーはアクションメソッドを実行して認可チェックを行います。
export default class PostPolicy extends BasePolicy {
async before(user: User | null, action: string, ...params: any[]) {
/**
* 管理者ユーザーは常にチェックを行わずに許可します。
*/
if (user && user.isAdmin) {
return true
}
}
}

afterメソッドはアクションメソッドからの生の応答を受け取り、新しい値を返すことで以前の応答を上書きできます。afterからの応答は次のように解釈されます。

  • trueの値は成功した認可と見なされ、以前の応答は破棄されます。
  • falseの値はアクセスが拒否されたと見なされ、以前の応答は破棄されます。
  • undefinedの戻り値の場合、バウンサーは以前の応答を引き続き使用します。
import { AuthorizerResponse } from '@adonisjs/bouncer/types'
export default class PostPolicy extends BasePolicy {
async after(
user: User | null,
action: string,
response: AuthorizerResponse,
...params: any[]
) {
if (user && user.isAdmin) {
return true
}
}
}

依存性の注入

ポリシークラスはIoCコンテナを使用して作成されるため、@injectデコレータを使用してポリシーコンストラクタ内で依存関係を型指定して注入できます。

import { inject } from '@adonisjs/core'
import { PermissionsResolver } from '#services/permissions_resolver'
@inject()
export class PostPolicy extends BasePolicy {
constructor(
protected permissionsResolver: PermissionsResolver
) {
super()
}
}

ポリシークラスがHTTPリクエスト中に作成される場合は、HttpContextのインスタンスも注入できます。

import { HttpContext } from '@adonisjs/core/http'
import { PermissionsResolver } from '#services/permissions_resolver'
@inject()
export class PostPolicy extends BasePolicy {
constructor(protected ctx: HttpContext) {
super()
}
}

AuthorizationExceptionのスロー

allowsメソッドとdeniesメソッドの代わりに、bouncer.authorizeメソッドを使用して認可チェックを実行することもできます。このメソッドは、チェックが失敗した場合にAuthorizationExceptionをスローします。

router.put('posts/:id', async ({ bouncer, params }) => {
const post = await Post.findOrFail(post)
await bouncer.authorize(editPost, post)
/**
* 例外が発生しなかった場合、ユーザーは投稿を編集できると見なすことができます。
*/
})

AdonisJSは、AuthorizationExceptionを使用して、次のコンテンツネゴシエーションルールにしたがって403 - ForbiddenのHTTPレスポンスに変換します。

  • Accept=application/jsonヘッダーを持つHTTPリクエストは、エラーメッセージの配列を受け取ります。各配列要素はmessageプロパティを持つオブジェクトです。

  • Accept=application/vnd.api+jsonヘッダーを持つHTTPリクエストは、JSON API仕様にしたがってフォーマットされたエラーメッセージの配列を受け取ります。

  • その他のリクエストは、プレーンテキストの応答メッセージを受け取ります。ただし、ステータスページを使用して、認可エラーのカスタムエラーページを表示することもできます。

また、グローバル例外ハンドラ内でAuthorizationExceptionエラーを自己処理することもできます。

import { errors } from '@adonisjs/bouncer'
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'
export default class HttpExceptionHandler extends ExceptionHandler {
protected debug = !app.inProduction
protected renderStatusPages = app.inProduction
async handle(error: unknown, ctx: HttpContext) {
if (error instanceof errors.E_AUTHORIZATION_FAILURE) {
return ctx
.response
.status(error.status)
.send(error.getResponseMessage(ctx))
}
return super.handle(error, ctx)
}
}

カスタム認可応答のカスタマイズ

アビリティやポリシーから真偽値を返す代わりに、AuthorizationResponseクラスを使用してカスタムのエラーレスポンスを構築することもできます。

AuthorizationResponseクラスを使用すると、カスタムのHTTPステータスコードとエラーメッセージを定義できます。

import User from '#models/user'
import Post from '#models/post'
import { Bouncer, AuthorizationResponse } from '@adonisjs/bouncer'
export const editPost = Bouncer.ability((user: User, post: Post) => {
if (user.id === post.userId) {
return true
}
return AuthorizationResponse.deny('投稿が見つかりません', 404)
})

@adonisjs/i18nパッケージを使用している場合は、.tメソッドを使用してローカライズされた応答を返すこともできます。HTTPリクエストに基づいてユーザーの言語に応じて、デフォルトのメッセージよりも翻訳メッセージが使用されます。

export const editPost = Bouncer.ability((user: User, post: Post) => {
if (user.id === post.userId) {
return true
}
return AuthorizationResponse
.deny('投稿が見つかりません', 404) // デフォルトのメッセージ
.t('errors.not_found') // 翻訳識別子
})

カスタム応答ビルダの使用

個々の認可チェックごとにカスタムエラーメッセージを定義する柔軟性は素晴らしいです。ただし、常に同じ応答を返す場合は、同じコードを繰り返すことになります。

そのため、Bouncerのデフォルトの応答ビルダを次のようにオーバーライドできます。

import { Bouncer, AuthorizationResponse } from '@adonisjs/bouncer'
Bouncer.responseBuilder = (response: boolean | AuthorizationResponse) => {
if (response instanceof AuthorizationResponse) {
return response
}
if (response === true) {
return AuthorizationResponse.allow()
}
return AuthorizationResponse
.deny('リソースが見つかりません', 404)
.t('errors.not_found')
}

アビリティとポリシーの事前登録

これまでのガイドでは、使用するたびに明示的にアビリティやポリシーをインポートしています。ただし、事前に登録すると、名前の文字列としてアビリティやポリシーを参照できます。

アビリティとポリシーを事前に登録することは、TypeScriptのコードベース内でのみ使用するよりもインポートを整理するために役立ちます。ただし、Edgeテンプレート内ではDXが向上します。

次のコード例は、ポリシーを事前に登録しない場合と登録する場合のEdgeテンプレートの比較です。

事前登録しない場合。あまりきれいではありません

{{-- 最初にアビリティをインポートします --}}
@let(editPost = (await import('#abilities/main')).editPost)
@can(editPost, post)
{{-- 投稿を編集できます --}}
@end

事前登録する場合

{{-- アビリティ名を文字列として参照します --}}
@can('editPost', post)
{{-- 投稿を編集できます --}}
@end

initialize_bouncer_middleware.tsファイルを開くと、Bouncerインスタンスを作成する際にすでにアビリティとポリシーをインポートして事前に登録していることがわかります。

import * as abilities from '#abilities/main'
import { policies } from '#policies/main'
export default InitializeBouncerMiddleware {
async handle(ctx, next) {
ctx.bouncer = new Bouncer(
() => ctx.auth.user,
abilities,
policies
)
}
}

注意点

  • コードベースの他の部分でアビリティを定義することを決定した場合は、ミドルウェア内でインポートして事前に登録するようにしてください。

  • ポリシーの場合、make:policyコマンドを実行するたびに、ポリシーをポリシーコレクション内に登録するようにプロンプトを受け入れることを確認してください。ポリシーコレクションは./app/policies/main.tsファイル内で定義されています。

    app/policies/main.ts
    export const policies = {
    PostPolicy: () => import('#polices/post_policy'),
    CommentPolicy: () => import('#polices/comment_policy')
    }

事前登録されたアビリティとポリシーの参照

次の例では、インポートを削除し、アビリティとポリシーを名前の文字列として参照しています。ただし、文字列ベースのAPIも型安全ですが、コードエディタの「定義に移動」機能は機能しない場合があります

アビリティの使用例
import { editPost } from '#abilities/main'
router.put('posts/:id', async ({ bouncer, params, response }) => {
const post = await Post.findOrFail(params.id)
if (await bouncer.allows(editPost, post)) {
if (await bouncer.allows('editPost', post)) {
return '投稿を編集できます。'
}
})
ポリシーの使用例
import PostPolicy from '#policies/post_policy'
export default class PostsController {
async create({ bouncer, response }: HttpContext) {
if (await bouncer.with(PostPolicy).denies('create')) {
if (await bouncer.with('PostPolicy').denies('create')) {
return response.forbidden('投稿を作成することはできません。')
}
//コントローラのロジックを続行します
}
}

インターフェイス内の認可チェック

Edgeテンプレート内で認可チェックを実行するには、アビリティとポリシーを事前に登録する必要があります。登録が完了したら、@canタグと@cannotタグを使用して認可チェックを実行できます。

これらのタグは、最初のパラメーターとしてability名またはpolicy.method名を受け入れ、アビリティまたはポリシーが受け入れる残りのパラメーターを続けます。

アビリティを使用した例
@can('editPost', post)
{{-- 投稿を編集できます --}}
@end
@cannot('editPost', post)
{{-- 投稿を編集できません --}}
@end
ポリシーを使用した例
@can('PostPolicy.edit', post)
{{-- 投稿を編集できます --}}
@end
@cannot('PostPolicy.edit', post)
{{-- 投稿を編集できません --}}
@end

イベント

@adonisjs/bouncerパッケージがディスパッチするイベントのリストを表示するには、イベントリファレンスガイドを参照してください。