Chapter 09無料公開

✅認可を設定する、認可をテストする

たった
たった
2021.06.24に更新
このチャプターの目次
アプリケーションコード
src
>├── middleware     ... 認証・認可とGraphQLのコンテキスト
├── domain         ... ビジネスロジックの共通化
├── usecases       ... アプリケーションロジック
├── infrastructure ... 外部サービスとのやりとり
├── entities       ... エンティティとGraphQLのフィールド
>├── resolvers      ... GraphQLのリゾルバー
└── inversify.config.ts ... 依存性の注入(以下、DI)の設定

このチャプターで使用するライブラリ

  • TypeGraphQL

認可を設定する

ユーザの権限ごとにアクセス可能なリソースを制御したい、という要件はよくあると思います。
先程Contextに渡した操作しているユーザの権限を使って制御していきましょう。

サンプルコード

Resolverに以下の2つのエンドポイントを作る場合を考えます。

  • ユーザを作成する
    • リクエストしてきたユーザを新しいユーザとして登録する
    • 認証されたユーザであれば、誰でもアクセスできる
  • ユーザを更新する
    • 他のユーザのプロパティを変更する
    • 権限が「管理者」のアカウントだけがアクセスできる

権限はこんな感じで単純にしてみます。

export enum UserRole {
  Admin = '管理者',
  Staff = '担当者',
}

とりあえず中身は空でResolverにエンドポイントを書いてみます。
「管理者」だけがアクセスできるエンドポイントには@Authorized()を付与します。

be/src/resolvers/userResolver.ts
import { 
  Arg, 
+  Authorized, 
  Ctx, 
  Mutation, 
  Resolver
} from 'type-graphql'

@Resolver(() => User)
export class UserResolver {
  @Mutation(() => User)
  async userCreate(
    @Ctx() ctx: IContext,
    @Arg('input') input: UserCreateInput
  ): Promise<User> {
  }

  @Mutation(() => [User])
+  @Authorized([UserRole.Admin])  // 認可を設定する
  async usersUpdate(
    @Arg('inputs') inputs: UsersUpdateInput[]
  ): Promise<User[]> {
  }
}

@Authorized()の中身を実装します。
currentUserの権限が、アクセス可能な権限ならtrueを返します。

be/src/middleware/userAuthorizationChecker
import { AuthChecker } from 'type-graphql'
import { UserRole } from 'src/entities/types/UserType'
import { IContext } from 'src/middleware/types/ContextType'

export const userAuthorizationChecker: AuthChecker<IContext, UserRole> = (
  { context },
  roles
) => {
  if (!context.currentUser) return false
  if (!roles.length) return true
  if (roles.includes(context.currentUser.role)) return true
  return false
}

スキーマのauthCheckerオプションに設定します。

be/src/middleware/buildSchema.ts
import { UserResolver } from 'src/resolvers/userResolver'
import { userAuthorizationChecker } from 'src/middleware/userAuhtorizatoinChekcer'

export const buildSchema = async (): Promise<GraphQLSchema> => (
  buildSchemaSync({
    authChecker: userAuthorizationChecker, // スキーマにCheckerを定義する
    resolvers: [UserResolver],
  })
)

これでリクエストに対して認可を設定することができました。

認可をテストする

では認可が正常に動作しているかどうか、テストで確認しましょう。

be/tests/userResolver.test.ts
const schema = await buildSchema()

const createMutation = `
  mutation userCreate($input: UserCreateInput!) {
    userCreate(input: $input) {
      id
      name
      email
      role
    }
  }
`

it.each`
  role               
  ${UserRole.Admin}
  ${UserRole.Staff}
  ${undefined}
`(
  '操作しているユーザの権限が$roleなら操作を認める',
  async ({ role }: { role: UserRole | undefined }) => {
    const dummyInput = {
      name: 'テスト太郎',
      email: 'dummy@example.com'
    }
    const actual = await graphql(
      schema,
      createMutation,
      {},
      { 
        currentUser: { 
          id: 1, 
	  role: role ? role : undefined 
	} 
      },
      dummyInput,
    )

    expect(actual.errors).toBeUndefined()
  }
)


const updateMutation = `
  mutation usersUpdate($inputs: [UsersUpdateInput]!) {
    usersUpdate(inputs: $inputs) {
      id
      name
      email
      role
    }
  }
`

it.each`
  role               
  ${UserRole.Admin}
`(
  '操作しているユーザの権限が$roleなら操作を認める',
  async ({ role }: { role: UserRole | undefined }) => {
    const dummyInput = [
      {
        name: '更新太郎',
      }
    ]
    const actual = await graphql(
      schema,
      createMutation,
      {},
      { 
        currentUser: { 
          id: 1, 
	  role: role ? role : undefined 
	} 
      },
      dummyInput,
    )

    expect(actual.errors).toBeUndefined()
  }
)

it.each`
  role             
  ${UserRole.Staff}
  ${undefined}
`(
  '操作しているユーザの権限が$roleなら操作を認めない',
  async ({ role }: { role: UserRole | undefined }) => {
    const dummyInput = [
      {
        name: '更新太郎',
      }
    ]

    const actual = await graphql(
      schema,
      updateMutation,
      {},
      { 
        currentUser: { 
          id: 1, 
	  role: role ? role : undefined 
	} 
      },
      dummyInput,
    )

    if (!actual.errors) return expect(false).toBeTruthy()
    expect(actual.errors[0].message).toContain('Access denied!')
  }
)

やや冗長ですが、これで操作しているユーザの権限を網羅することができました。
後はテストを実行しましょう。

/be
$ yarn test
# ... Done!