Merged PR 620: テンプレートファイルダウンロードをTypistのみが実行可能にする

## 概要
[Task3291: テンプレートファイルダウンロードをTypistのみが実行可能にする](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3291)

- テンプレートファイルダウンロード先要求APIを実行できるユーザーをTypistのみに修正しました。
  - Authorが実行できないようにしました。

## レビューポイント
- ガードでTypistのみにしたので内部のロールでの分岐処理を削除しましたが問題ないでしょうか?

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2023-12-13 08:08:58 +00:00
parent 0f35789b91
commit 63892bad83
3 changed files with 137 additions and 198 deletions

View File

@ -338,9 +338,7 @@ export class FilesController {
}) })
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@UseGuards( @UseGuards(RoleGuard.requireds({ roles: [USER_ROLES.TYPIST] }))
RoleGuard.requireds({ roles: [USER_ROLES.AUTHOR, USER_ROLES.TYPIST] }),
)
async downloadTemplateLocation( async downloadTemplateLocation(
@Req() req: Request, @Req() req: Request,
@Query() body: TemplateDownloadLocationRequest, @Query() body: TemplateDownloadLocationRequest,

View File

@ -34,7 +34,12 @@ import { TasksRepositoryService } from '../../repositories/tasks/tasks.repositor
import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service'; import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service';
import { getCheckoutPermissions, getTask } from '../tasks/test/utility'; import { getCheckoutPermissions, getTask } from '../tasks/test/utility';
import { DateWithZeroTime } from '../licenses/types/types'; import { DateWithZeroTime } from '../licenses/types/types';
import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../constants'; import {
LICENSE_ALLOCATED_STATUS,
LICENSE_TYPE,
TASK_STATUS,
USER_ROLES,
} from '../../constants';
describe('publishUploadSas', () => { describe('publishUploadSas', () => {
let source: DataSource | null = null; let source: DataSource | null = null;
@ -1505,15 +1510,11 @@ describe('テンプレートファイルダウンロードURL取得', () => {
it('ダウンロードSASトークンが乗っているURLを取得できる', async () => { it('ダウンロードSASトークンが乗っているURLを取得できる', async () => {
if (!source) fail(); if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source); const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, author_id: authorId } = await makeTestUser( const { external_id: externalId, id: userId } = await makeTestUser(source, {
source,
{
account_id: accountId, account_id: accountId,
external_id: 'author-user-external-id', external_id: 'typist-user-external-id',
role: 'author', role: USER_ROLES.TYPIST,
author_id: 'AUTHOR_ID', });
},
);
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`; const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`;
const { audioFileId } = await createTask( const { audioFileId } = await createTask(
@ -1521,9 +1522,9 @@ describe('テンプレートファイルダウンロードURL取得', () => {
accountId, accountId,
url, url,
'test.zip', 'test.zip',
'InProgress', TASK_STATUS.IN_PROGRESS,
undefined, userId,
authorId ?? '', 'AUTHOR_ID',
); );
const blobParam = makeBlobstorageServiceMockValue(); const blobParam = makeBlobstorageServiceMockValue();
@ -1539,13 +1540,12 @@ describe('テンプレートファイルダウンロードURL取得', () => {
if (!module) fail(); if (!module) fail();
const service = module.get<FilesService>(FilesService); const service = module.get<FilesService>(FilesService);
expect( const resultUrl = await service.publishTemplateFileDownloadSas(
await service.publishTemplateFileDownloadSas(
makeContext('tracking', 'requestId'), makeContext('tracking', 'requestId'),
externalId, externalId,
audioFileId, audioFileId,
), );
).toEqual(`${url}?sas-token`); expect(resultUrl).toBe(`${url}?sas-token`);
}); });
it('ダウンロードSASトークンが乗っているURLを取得できる第五階層の場合ライセンスのチェックを行う', async () => { it('ダウンロードSASトークンが乗っているURLを取得できる第五階層の場合ライセンスのチェックを行う', async () => {
if (!source) fail(); if (!source) fail();
@ -1557,15 +1557,10 @@ describe('テンプレートファイルダウンロードURL取得', () => {
parent_account_id: tier4Accounts[0].account.id, parent_account_id: tier4Accounts[0].account.id,
tier: 5, tier: 5,
}); });
const { const { external_id: externalId, id: userId } = await makeTestUser(source, {
external_id: externalId,
id: userId,
author_id: authorId,
} = await makeTestUser(source, {
account_id: tier5Accounts.account.id, account_id: tier5Accounts.account.id,
external_id: 'author-user-external-id', external_id: 'typist-user-external-id',
role: 'author', role: USER_ROLES.TYPIST,
author_id: 'AUTHOR_ID',
}); });
// 本日の日付を作成 // 本日の日付を作成
let yesterday = new Date(); let yesterday = new Date();
@ -1591,9 +1586,9 @@ describe('テンプレートファイルダウンロードURL取得', () => {
tier5Accounts.account.id, tier5Accounts.account.id,
url, url,
'test.zip', 'test.zip',
'InProgress', TASK_STATUS.IN_PROGRESS,
undefined, userId,
authorId ?? '', 'AUTHOR_ID',
); );
const blobParam = makeBlobstorageServiceMockValue(); const blobParam = makeBlobstorageServiceMockValue();
@ -1609,22 +1604,20 @@ describe('テンプレートファイルダウンロードURL取得', () => {
if (!module) fail(); if (!module) fail();
const service = module.get<FilesService>(FilesService); const service = module.get<FilesService>(FilesService);
expect( const resultUrl = await service.publishTemplateFileDownloadSas(
await service.publishTemplateFileDownloadSas(
makeContext('trackingId', 'requestId'), makeContext('trackingId', 'requestId'),
externalId, externalId,
audioFileId, audioFileId,
), );
).toEqual(`${url}?sas-token`); expect(resultUrl).toBe(`${url}?sas-token`);
}); });
it('Typistの場合、タスクのステータスが[Inprogress,Pending]以外でエラー', async () => { it('タスクのステータスが[Inprogress,Pending]以外でエラー', async () => {
if (!source) fail(); if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source); const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, id: userId } = await makeTestUser(source, { const { external_id: externalId, id: userId } = await makeTestUser(source, {
account_id: accountId, account_id: accountId,
external_id: 'typist-user-external-id', external_id: 'typist-user-external-id',
role: 'typist', role: USER_ROLES.TYPIST,
author_id: undefined,
}); });
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`; const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`;
@ -1633,7 +1626,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
accountId, accountId,
url, url,
'test.zip', 'test.zip',
'Finished', TASK_STATUS.FINISHED,
userId, userId,
); );
@ -1650,31 +1643,35 @@ describe('テンプレートファイルダウンロードURL取得', () => {
if (!module) fail(); if (!module) fail();
const service = module.get<FilesService>(FilesService); const service = module.get<FilesService>(FilesService);
await expect( try {
service.publishTemplateFileDownloadSas( await service.publishTemplateFileDownloadSas(
makeContext('tracking', 'requestId'), makeContext('tracking', 'requestId'),
externalId, externalId,
audioFileId, audioFileId,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010603'), HttpStatus.BAD_REQUEST),
); );
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E010603'));
} else {
fail();
}
}
}); });
it('Typistの場合、自身が担当するタスクでない場合エラー', async () => { it('自身が担当するタスクでない場合エラー', async () => {
if (!source) fail(); if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source); const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, { const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId, account_id: accountId,
external_id: 'typist-user-external-id', external_id: 'typist-user-external-id',
role: 'typist', role: USER_ROLES.TYPIST,
author_id: undefined,
}); });
const { id: otherId } = await makeTestUser(source, { const { id: otherId } = await makeTestUser(source, {
account_id: accountId, account_id: accountId,
external_id: 'other-typist-user-external-id', external_id: 'other-typist-user-external-id',
role: 'typist', role: USER_ROLES.TYPIST,
author_id: undefined,
}); });
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`; const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`;
@ -1683,7 +1680,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
accountId, accountId,
url, url,
'test.zip', 'test.zip',
'InProgress', TASK_STATUS.IN_PROGRESS,
otherId, otherId,
); );
@ -1700,60 +1697,21 @@ describe('テンプレートファイルダウンロードURL取得', () => {
if (!module) fail(); if (!module) fail();
const service = module.get<FilesService>(FilesService); const service = module.get<FilesService>(FilesService);
await expect( try {
service.publishTemplateFileDownloadSas( await service.publishTemplateFileDownloadSas(
makeContext('tracking', 'requestId'), makeContext('tracking', 'requestId'),
externalId, externalId,
audioFileId, audioFileId,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010603'), HttpStatus.BAD_REQUEST),
);
});
it('Authorの場合、自身が登録したタスクでない場合エラー', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`;
const { audioFileId } = await createTask(
source,
accountId,
url,
'test.zip',
'InProgress',
undefined,
'OTHOR_ID',
);
const blobParam = makeBlobstorageServiceMockValue();
blobParam.publishDownloadSas = `${url}?sas-token`;
blobParam.fileExists = true;
const notificationParam = makeDefaultNotificationhubServiceMockValue();
const module = await makeTestingModuleWithBlobAndNotification(
source,
blobParam,
notificationParam,
);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
service.publishTemplateFileDownloadSas(
makeContext('tracking', 'requestId'),
externalId,
audioFileId,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010603'), HttpStatus.BAD_REQUEST),
); );
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E010603'));
} else {
fail();
}
}
}); });
it('Taskが存在しない場合はエラーとなる', async () => { it('Taskが存在しない場合はエラーとなる', async () => {
@ -1761,9 +1719,8 @@ describe('テンプレートファイルダウンロードURL取得', () => {
const { id: accountId } = await makeTestSimpleAccount(source); const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, { const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId, account_id: accountId,
external_id: 'author-user-external-id', external_id: 'typist-user-external-id',
role: 'author', role: USER_ROLES.TYPIST,
author_id: 'AUTHOR_ID',
}); });
const blobParam = makeBlobstorageServiceMockValue(); const blobParam = makeBlobstorageServiceMockValue();
@ -1777,29 +1734,31 @@ describe('テンプレートファイルダウンロードURL取得', () => {
if (!module) fail(); if (!module) fail();
const service = module.get<FilesService>(FilesService); const service = module.get<FilesService>(FilesService);
await expect( try {
service.publishTemplateFileDownloadSas( await service.publishTemplateFileDownloadSas(
makeContext('tracking', 'requestId'), makeContext('tracking', 'requestId'),
externalId, externalId,
1, 1,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010603'), HttpStatus.BAD_REQUEST),
); );
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E010603'));
} else {
fail();
}
}
}); });
it('blobストレージにファイルが存在しない場合はエラーとなる', async () => { it('blobストレージにファイルが存在しない場合はエラーとなる', async () => {
if (!source) fail(); if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source); const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, author_id: authorId } = await makeTestUser( const { external_id: externalId, id: userId } = await makeTestUser(source, {
source,
{
account_id: accountId, account_id: accountId,
external_id: 'author-user-external-id', external_id: 'typist-user-external-id',
role: 'author', role: USER_ROLES.TYPIST,
author_id: 'AUTHOR_ID', });
},
);
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`; const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`;
const { audioFileId } = await createTask( const { audioFileId } = await createTask(
@ -1807,9 +1766,9 @@ describe('テンプレートファイルダウンロードURL取得', () => {
accountId, accountId,
url, url,
'test.zip', 'test.zip',
'InProgress', TASK_STATUS.IN_PROGRESS,
undefined, userId,
authorId ?? '', 'AUTHOR_ID',
); );
const blobParam = makeBlobstorageServiceMockValue(); const blobParam = makeBlobstorageServiceMockValue();
@ -1825,15 +1784,21 @@ describe('テンプレートファイルダウンロードURL取得', () => {
if (!module) fail(); if (!module) fail();
const service = module.get<FilesService>(FilesService); const service = module.get<FilesService>(FilesService);
await expect( try {
service.publishTemplateFileDownloadSas( await service.publishTemplateFileDownloadSas(
makeContext('tracking', 'requestId'), makeContext('tracking', 'requestId'),
externalId, externalId,
audioFileId, audioFileId,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010701'), HttpStatus.BAD_REQUEST),
); );
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E010701'));
} else {
fail();
}
}
}); });
it('ダウンロード時にユーザーにライセンスが未割当の場合エラーとなる(第五階層限定)', async () => { it('ダウンロード時にユーザーにライセンスが未割当の場合エラーとなる(第五階層限定)', async () => {
if (!source) fail(); if (!source) fail();
@ -1845,15 +1810,10 @@ describe('テンプレートファイルダウンロードURL取得', () => {
parent_account_id: tier4Accounts[0].account.id, parent_account_id: tier4Accounts[0].account.id,
tier: 5, tier: 5,
}); });
const { const { external_id: externalId, id: userId } = await makeTestUser(source, {
external_id: externalId,
id: userId,
author_id: authorId,
} = await makeTestUser(source, {
account_id: tier5Accounts.account.id, account_id: tier5Accounts.account.id,
external_id: 'author-user-external-id', external_id: 'typist-user-external-id',
role: 'author', role: USER_ROLES.TYPIST,
author_id: 'AUTHOR_ID',
}); });
const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`; const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`;
@ -1862,9 +1822,9 @@ describe('テンプレートファイルダウンロードURL取得', () => {
tier5Accounts.account.id, tier5Accounts.account.id,
url, url,
'test.zip', 'test.zip',
'InProgress', TASK_STATUS.IN_PROGRESS,
undefined, undefined,
authorId ?? '', 'AUTHOR_ID',
); );
const blobParam = makeBlobstorageServiceMockValue(); const blobParam = makeBlobstorageServiceMockValue();
@ -1880,15 +1840,21 @@ describe('テンプレートファイルダウンロードURL取得', () => {
if (!module) fail(); if (!module) fail();
const service = module.get<FilesService>(FilesService); const service = module.get<FilesService>(FilesService);
await expect( try {
service.publishTemplateFileDownloadSas( await service.publishTemplateFileDownloadSas(
makeContext('trackingId', 'requestId'), makeContext('trackingId', 'requestId'),
externalId, externalId,
audioFileId, audioFileId,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010812'), HttpStatus.BAD_REQUEST),
); );
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E010812'));
} else {
fail();
}
}
}); });
it('ダウンロード時にユーザーに割り当てられたライセンスが有効期限切れの場合エラー(第五階層限定)', async () => { it('ダウンロード時にユーザーに割り当てられたライセンスが有効期限切れの場合エラー(第五階層限定)', async () => {
if (!source) fail(); if (!source) fail();
@ -1900,15 +1866,10 @@ describe('テンプレートファイルダウンロードURL取得', () => {
parent_account_id: tier4Accounts[0].account.id, parent_account_id: tier4Accounts[0].account.id,
tier: 5, tier: 5,
}); });
const { const { external_id: externalId, id: userId } = await makeTestUser(source, {
external_id: externalId,
id: userId,
author_id: authorId,
} = await makeTestUser(source, {
account_id: tier5Accounts.account.id, account_id: tier5Accounts.account.id,
external_id: 'author-user-external-id', external_id: 'typist-user-external-id',
role: 'author', role: USER_ROLES.TYPIST,
author_id: 'AUTHOR_ID',
}); });
// 昨日の日付を作成 // 昨日の日付を作成
let yesterday = new Date(); let yesterday = new Date();
@ -1934,9 +1895,9 @@ describe('テンプレートファイルダウンロードURL取得', () => {
tier5Accounts.account.id, tier5Accounts.account.id,
url, url,
'test.zip', 'test.zip',
'InProgress', TASK_STATUS.IN_PROGRESS,
undefined, undefined,
authorId ?? '', 'AUTHOR_ID',
); );
const blobParam = makeBlobstorageServiceMockValue(); const blobParam = makeBlobstorageServiceMockValue();
@ -1952,15 +1913,21 @@ describe('テンプレートファイルダウンロードURL取得', () => {
if (!module) fail(); if (!module) fail();
const service = module.get<FilesService>(FilesService); const service = module.get<FilesService>(FilesService);
await expect( try {
service.publishTemplateFileDownloadSas( await service.publishTemplateFileDownloadSas(
makeContext('trackingId', 'requestId'), makeContext('trackingId', 'requestId'),
externalId, externalId,
audioFileId, audioFileId,
), ),
).rejects.toEqual( fail();
new HttpException(makeErrorResponse('E010805'), HttpStatus.BAD_REQUEST), } catch (e) {
); if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E010805'));
} else {
fail();
}
}
}); });
}); });

View File

@ -474,7 +474,7 @@ export class FilesService {
// ユーザーがTypistの場合、自身が担当したタスクでない場合はエラー // ユーザーがTypistの場合、自身が担当したタスクでない場合はエラー
if (isTypist && task.typist_user_id !== userId) { if (isTypist && task.typist_user_id !== userId) {
throw new AuthorUserNotMatchError( throw new TypistUserNotFoundError(
`task typist is not match. audio_file_id:${audioFileId}, task.typist_user_id:${task.typist_user_id}, userId:${userId}`, `task typist is not match. audio_file_id:${audioFileId}, task.typist_user_id:${task.typist_user_id}, userId:${userId}`,
); );
} }
@ -563,8 +563,6 @@ export class FilesService {
let accountId: number; let accountId: number;
let userId: number; let userId: number;
let country: string; let country: string;
let isTypist: boolean;
let authorId: string | undefined;
try { try {
const user = await this.usersRepository.findUserByExternalId( const user = await this.usersRepository.findUserByExternalId(
context, context,
@ -590,8 +588,6 @@ export class FilesService {
accountId = user.account_id; accountId = user.account_id;
userId = user.id; userId = user.id;
country = user.account.country; country = user.account.country;
isTypist = user.role === USER_ROLES.TYPIST;
authorId = user.author_id ?? undefined;
} catch (e) { } catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`); this.logger.error(`[${context.getTrackingId()}] error=${e}`);
this.logger.log( this.logger.log(
@ -619,27 +615,13 @@ export class FilesService {
} }
try { try {
const status = isTypist
? [TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING]
: Object.values(TASK_STATUS);
const task = await this.tasksRepository.getTaskAndAudioFile( const task = await this.tasksRepository.getTaskAndAudioFile(
context, context,
audioFileId, audioFileId,
accountId, accountId,
status, [TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING],
); );
const { file } = task; const { template_file } = task;
// タスクに紐づく音声ファイルだけが消される場合がある。
// その場合はダウンロード不可なので不在エラーとして扱う
if (!file) {
throw new AudioFileNotFoundError(
`Audio file is not exists in DB. audio_file_id:${audioFileId}`,
);
}
const template_file = task.template_file;
// タスクに紐づくテンプレートファイルがない場合がある。 // タスクに紐づくテンプレートファイルがない場合がある。
// その場合はダウンロード不可なので不在エラーとして扱う // その場合はダウンロード不可なので不在エラーとして扱う
@ -649,16 +631,9 @@ export class FilesService {
); );
} }
// ユーザーがAuthorの場合、自身が追加したタスクでない場合はエラー // ユーザー自身が担当したタスクでない場合はエラー
if (!isTypist && file.author_id !== authorId) { if (task.typist_user_id !== userId) {
throw new AuthorUserNotMatchError( throw new TypistUserNotFoundError(
`task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${file.author_id}, authorId:${authorId}`,
);
}
// ユーザーがTypistの場合、自身が担当したタスクでない場合はエラー
if (isTypist && task.typist_user_id !== userId) {
throw new AuthorUserNotMatchError(
`task typist is not match. audio_file_id:${audioFileId}, task.typist_user_id:${task.typist_user_id}, userId:${userId}`, `task typist is not match. audio_file_id:${audioFileId}, task.typist_user_id:${task.typist_user_id}, userId:${userId}`,
); );
} }
@ -693,7 +668,6 @@ export class FilesService {
case TasksNotFoundError: case TasksNotFoundError:
case AccountNotMatchError: case AccountNotMatchError:
case StatusNotMatchError: case StatusNotMatchError:
case AuthorUserNotMatchError:
case TypistUserNotFoundError: case TypistUserNotFoundError:
throw new HttpException( throw new HttpException(
makeErrorResponse('E010603'), makeErrorResponse('E010603'),