Merged PR 139: 認証・認可を宣言的に扱える仕組みを既存処理に適用する

## 概要
[Task1830: 認証・認可を宣言的に扱える仕組みを既存処理に適用する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1830)

- 既存処理について、認証認可をガードで実施するよう以下のAPIに適用しました。
- ユーザー一覧
  -  GET /users
- ユーザー追加
  - POST /users/signup/
- 音声ファイルアップロード先取得
  - GET /files/audio/upload-location/
 - ライセンス注文
   - POST /licenses/orders
ロールガードのテストで定数を使うように修正

## レビューポイント
- 対応APIの抜け漏れはないか
- 対応内容に問題はないか
  - 付与権限
  - トークン内容取得

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2023-06-09 08:39:44 +00:00
parent 15ee0c2e98
commit 1082a48fe9
7 changed files with 64 additions and 84 deletions

View File

@ -1,11 +0,0 @@
/**
*
* @param {string[]}
* @return {boolean}
*/
// XXX: deprecated 削除予定
export const confirmPermission = (authority: string): boolean => {
console.log(authority);
return true;
};

View File

@ -1,17 +1,18 @@
import { ADMIN_ROLES, USER_ROLES } from '../../../constants';
import { RoleGuard } from './roleguards';
describe('RoleGuard', () => {
it('1つの許可Roleが設定時、完全に一致するroleを持つ場合、許可される', () => {
const guards = RoleGuard.requireds({ roles: ['author'] });
const guards = RoleGuard.requireds({ roles: [USER_ROLES.AUTHOR] });
expect(guards.checkRole('author')).toBeTruthy();
});
it('1つの許可Roleが設定時、その許可roleを含むroleを持つ場合、許可される', () => {
const guards = RoleGuard.requireds({ roles: ['author'] });
const guards = RoleGuard.requireds({ roles: [USER_ROLES.AUTHOR] });
// 'author admin'が許可リスト(author)に含まれるので許可
expect(guards.checkRole('author admin')).toBeTruthy();
});
it('author OR adminの許可Roleが設定時、その許可roleを含むroleを持つ場合、許可される', () => {
const guards = RoleGuard.requireds({ roles: ['author', 'admin'] });
const guards = RoleGuard.requireds({ roles: [USER_ROLES.AUTHOR, ADMIN_ROLES.ADMIN] });
// authorが許可リスト([authorまたはadmin])に含まれるので許可
expect(guards.checkRole('author')).toBeTruthy();
// adminが許可リスト([authorまたはadmin])に含まれるので許可
@ -20,19 +21,25 @@ describe('RoleGuard', () => {
expect(guards.checkRole('author admin')).toBeTruthy();
});
it('author OR adminの許可Roleが設定時、その許可roleを含むroleを持たない場合、拒否される', () => {
const guards = RoleGuard.requireds({ roles: ['author', 'admin'] });
const guards = RoleGuard.requireds({
roles: [USER_ROLES.AUTHOR, ADMIN_ROLES.ADMIN],
});
// typistが許可リスト([authorまたはadmin])に含まれないので拒否
expect(guards.checkRole('typist')).toBeFalsy();
});
it('author AND adminの許可Roleが設定時、その許可roleを含むroleを持つ場合、許可される', () => {
const guards = RoleGuard.requireds({ roles: [['author', 'admin']] });
const guards = RoleGuard.requireds({
roles: [[USER_ROLES.AUTHOR, ADMIN_ROLES.ADMIN]],
});
// 'author admin'が許可リスト([authorかつadmin])に含まれるので許可
expect(guards.checkRole('author admin')).toBeTruthy();
// 'typist author admin'が許可リスト([authorかつadmin])に含まれるので許可
expect(guards.checkRole('typist author admin')).toBeTruthy();
});
it('author AND adminの許可Roleが設定時、その許可roleに合致しないroleを持つ場合、拒否される', () => {
const guards = RoleGuard.requireds({ roles: [['author', 'admin']] });
const guards = RoleGuard.requireds({
roles: [[USER_ROLES.AUTHOR, ADMIN_ROLES.ADMIN]],
});
// authorが許可リスト([authorかつadmin])に含まれないので拒否
expect(guards.checkRole('author')).toBeFalsy();
// adminが許可リスト([authorかつadmin])に含まれないので拒否
@ -42,7 +49,7 @@ describe('RoleGuard', () => {
});
it('(author AND admin) OR typistの許可Roleが設定時、その許可roleを含むroleを持つ場合、許可される', () => {
const guards = RoleGuard.requireds({
roles: [['author', 'admin'], 'typist'],
roles: [[USER_ROLES.AUTHOR, ADMIN_ROLES.ADMIN], USER_ROLES.TYPIST],
});
// typistが許可リスト(typist)に含まれないので許可
expect(guards.checkRole('typist')).toBeTruthy();
@ -53,7 +60,7 @@ describe('RoleGuard', () => {
});
it('(author AND admin) OR typistの許可Roleが設定時、その許可roleを含むroleを持たない場合、拒否される', () => {
const guards = RoleGuard.requireds({
roles: [['author', 'admin'], 'typist'],
roles: [[USER_ROLES.AUTHOR, ADMIN_ROLES.ADMIN], USER_ROLES.TYPIST],
});
// authorが許可リスト([authorかつadmin])に含まれないので拒否
expect(guards.checkRole('author')).toBeFalsy();

View File

@ -98,9 +98,9 @@ export class AuthController {
})
async accessToken(@Req() req): Promise<AccessTokenResponse> {
const refreshToken = retrieveAuthorizationToken(req);
if (refreshToken !== undefined) {
if (!refreshToken) {
throw new HttpException(
makeErrorResponse('E009999'),
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}

View File

@ -145,7 +145,11 @@ export class AuthService {
const token = verify<RefreshToken>(refreshToken, pubkey);
if (isVerifyError(token)) {
throw new Error(`${token.reason} | ${token.message}`);
this.logger.error(`${token.reason} | ${token.message}`);
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const accessToken = sign<AccessToken>(

View File

@ -30,6 +30,7 @@ import {
} from './types/types';
import { AuthGuard } from '../../common/guards/auth/authguards';
import { RoleGuard } from '../../common/guards/role/roleguards';
import { USER_ROLES } from '../../constants';
@ApiTags('files')
@Controller('files')
@ -63,7 +64,7 @@ export class FilesController {
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: ['author'] }))
@UseGuards(RoleGuard.requireds({ roles: [USER_ROLES.AUTHOR] }))
@Post('audio/upload-finished')
async uploadFinished(
@Headers('authorization') authorization: string,
@ -134,12 +135,14 @@ export class FilesController {
'ログイン中ユーザー用のBlob Storage上の音声ファイルのアップロード先アクセスURLを取得します',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [USER_ROLES.AUTHOR] }))
async uploadLocation(
@Headers('authorization') authorization: string,
@Query() query: AudioUploadLocationRequest,
// クエリパラメータ AudioUploadLocationRequest は空であるため内部で使用しない。
// 使用しないことを宣言するために先頭にプレフィックス_アンダースコアをつけている
@Query() _query: AudioUploadLocationRequest,
): Promise<AudioUploadLocationResponse> {
const {} = query;
//TODO Guardsで認証するからここではデコードするだけ
const token = authorization.substring(
'Bearer '.length,
authorization.length,

View File

@ -8,21 +8,22 @@ import {
UseGuards,
} from '@nestjs/common';
import {
ApiBearerAuth,
ApiOperation,
ApiResponse,
ApiTags,
ApiOperation,
ApiBearerAuth,
} from '@nestjs/swagger';
import { Request } from 'express';
import { decode } from 'jsonwebtoken';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { ErrorResponse } from '../../common/error/types/types';
import { AuthGuard } from '../../common/guards/auth/authguards';
import { RoleGuard } from '../../common/guards/role/roleguards';
import { LicensesService } from './licenses.service';
import { CreateOrdersResponse, CreateOrdersRequest } from './types/types';
import { Request } from 'express';
import { retrieveAuthorizationToken } from '../../common/http/helper';
import { AccessToken } from '../../common/token';
import { LicensesService } from './licenses.service';
import { CreateOrdersRequest, CreateOrdersResponse } from './types/types';
import { AuthGuard } from '../../common/guards/auth/authguards';
import { RoleGuard } from '../../common/guards/role/roleguards';
import { ADMIN_ROLES } from '../../constants';
import jwt from 'jsonwebtoken';
@ApiTags('licenses')
@Controller('licenses')
@ -51,7 +52,7 @@ export class LicensesController {
@ApiOperation({ operationId: 'createOrders' })
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: ['admin', 'author'] }))
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Post('/orders')
async createOrders(
@Req() req: Request,
@ -60,22 +61,13 @@ export class LicensesController {
console.log(req.header('Authorization'));
console.log(body);
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
const accessToken = retrieveAuthorizationToken(req);
//アクセストークンが存在しない場合のエラー
if (accessToken == undefined) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedToken = decode(accessToken, {
json: true,
}) as AccessToken;
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
// ライセンス注文処理
await this.licensesService.licenseOrders(
decodedToken,
payload,
body.poNumber,
body.orderCount,
);

View File

@ -16,30 +16,31 @@ import {
ApiTags,
} from '@nestjs/swagger';
import { Request } from 'express';
import { decode } from 'jsonwebtoken';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { ErrorResponse } from '../../common/error/types/types';
import { AuthGuard } from '../../common/guards/auth/authguards';
import { RoleGuard } from '../../common/guards/role/roleguards';
import { retrieveAuthorizationToken } from '../../common/http/helper';
import { AccessToken } from '../../common/token';
import {
isSortDirection,
isTaskListSortableAttribute,
} from '../../common/types/sort';
import {
ConfirmRequest,
ConfirmResponse,
GetRelationsResponse,
GetSortCriteriaRequest,
GetSortCriteriaResponse,
GetUsersResponse,
PostSortCriteriaRequest,
PostSortCriteriaResponse,
SignupRequest,
SignupResponse,
PostSortCriteriaRequest,
PostSortCriteriaResponse,
GetSortCriteriaRequest,
GetSortCriteriaResponse,
} from './types/types';
import { UsersService } from './users.service';
import jwt from 'jsonwebtoken';
import { AuthGuard } from '../../common/guards/auth/authguards';
import {
isSortDirection,
isTaskListSortableAttribute,
} from '../../common/types/sort';
import { ADMIN_ROLES } from '../../constants';
import { RoleGuard } from '../../common/guards/role/roleguards';
@ApiTags('users')
@Controller('users')
@ -111,21 +112,13 @@ export class UsersController {
@ApiOperation({ operationId: 'getUsers' })
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: ['admin', 'author'] }))
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Get()
async getUsers(@Req() req: Request): Promise<GetUsersResponse> {
console.log(req.header('Authorization'));
const accessToken = retrieveAuthorizationToken(req);
// アクセストークンが存在しない場合のエラー
if (accessToken == undefined) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedToken = decode(accessToken, { json: true }) as AccessToken;
const decodedToken = jwt.decode(accessToken, { json: true }) as AccessToken;
const users = await this.usersService.getUsers(decodedToken);
return { users };
@ -153,9 +146,9 @@ export class UsersController {
})
@ApiOperation({ operationId: 'signup' })
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: ['admin', 'author'] }))
@Post('/signup')
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
async signup(
@Req() req: Request,
@Body() body: SignupRequest,
@ -172,19 +165,11 @@ export class UsersController {
} = body;
const accessToken = retrieveAuthorizationToken(req);
// アクセストークンが存在しない場合のエラー
if (accessToken == undefined) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedToken = decode(accessToken, { json: true }) as AccessToken;
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
//ユーザ作成処理
await this.usersService.createUser(
decodedToken,
payload,
name,
role,
email,
@ -276,7 +261,7 @@ export class UsersController {
): Promise<PostSortCriteriaResponse> {
const { direction, paramName } = body;
const accessToken = retrieveAuthorizationToken(req);
const decodedToken = decode(accessToken, { json: true }) as AccessToken;
const decodedToken = jwt.decode(accessToken, { json: true }) as AccessToken;
//型チェック
if (
@ -324,7 +309,7 @@ export class UsersController {
): Promise<GetSortCriteriaResponse> {
const {} = query;
const accessToken = retrieveAuthorizationToken(req);
const decodedToken = decode(accessToken, { json: true }) as AccessToken;
const decodedToken = jwt.decode(accessToken, { json: true }) as AccessToken;
const { direction, paramName } = await this.usersService.getSortCriteria(
decodedToken,