Merged PR 537: API実装(代行操作用トークン更新API)
## 概要 [Task2906: API実装(代行操作用トークン更新API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2906) - アクセストークン更新APIとテストを実装しました。 ## レビューポイント - リポジトリのアカウントチェックは適切か - テストケースは適切か ## UIの変更 - なし ## 動作確認状況 - ローカルで確認
This commit is contained in:
parent
f33af7a9cd
commit
01d92b2408
@ -32,7 +32,7 @@ import { AuthGuard } from '../../common/guards/auth/authguards';
|
||||
import { RoleGuard } from '../../common/guards/role/roleguards';
|
||||
import { ADMIN_ROLES, TIERS } from '../../constants';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { AccessToken } from '../../common/token';
|
||||
import { AccessToken, RefreshToken } from '../../common/token';
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller('auth')
|
||||
@ -248,9 +248,23 @@ export class AuthController {
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const decodedRefreshToken = jwt.decode(refreshToken, { json: true });
|
||||
if (!decodedRefreshToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const { userId, delegateUserId } = decodedRefreshToken as RefreshToken;
|
||||
|
||||
const context = makeContext(uuidv4());
|
||||
const context = makeContext(userId);
|
||||
const accessToken = await this.authService.updateDelegationAccessToken(
|
||||
context,
|
||||
delegateUserId,
|
||||
userId,
|
||||
refreshToken,
|
||||
);
|
||||
|
||||
return { accessToken: '' };
|
||||
return { accessToken };
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,9 +9,13 @@ import {
|
||||
import { DataSource } from 'typeorm';
|
||||
import { makeContext } from '../../common/log';
|
||||
import { makeTestingModule } from '../../common/test/modules';
|
||||
import { makeTestAccount } from '../../common/test/utility';
|
||||
import { getAccount, makeTestAccount } from '../../common/test/utility';
|
||||
import { AuthService } from './auth.service';
|
||||
import { createTermInfo } from './test/utility';
|
||||
import {
|
||||
createTermInfo,
|
||||
deleteAccount,
|
||||
updateAccountDelegationPermission,
|
||||
} from './test/utility';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { TIERS, USER_ROLES } from '../../constants';
|
||||
import { decode, isVerifyError } from '../../common/jwt';
|
||||
@ -512,6 +516,214 @@ describe('generateDelegationAccessToken', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateDelegationAccessToken', () => {
|
||||
let source: DataSource | null = null;
|
||||
beforeEach(async () => {
|
||||
source = new DataSource({
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
logging: false,
|
||||
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
|
||||
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
|
||||
});
|
||||
return source.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (!source) return;
|
||||
await source.destroy();
|
||||
source = null;
|
||||
});
|
||||
|
||||
it('代行操作用リフレッシュトークンから代行操作用アクセストークンを更新できること', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
const service = module.get<AuthService>(AuthService);
|
||||
const { admin: parentAdmin, account: parentAccount } =
|
||||
await makeTestAccount(source, {
|
||||
tier: 4,
|
||||
});
|
||||
const { admin: partnerAdmin, account: partnerAccount } =
|
||||
await makeTestAccount(
|
||||
source,
|
||||
{
|
||||
tier: 5,
|
||||
parent_account_id: parentAccount.id,
|
||||
delegation_permission: true,
|
||||
},
|
||||
{ role: USER_ROLES.NONE },
|
||||
);
|
||||
|
||||
const context = makeContext(parentAdmin.external_id);
|
||||
|
||||
const delegationRefreshToken = await service.generateDelegationRefreshToken(
|
||||
context,
|
||||
parentAdmin.external_id,
|
||||
partnerAccount.id,
|
||||
);
|
||||
|
||||
// 取得できた代行操作用リフレッシュトークンをデコード
|
||||
const decodeRefreshToken = decode<RefreshToken>(delegationRefreshToken);
|
||||
if (isVerifyError(decodeRefreshToken)) {
|
||||
fail();
|
||||
}
|
||||
|
||||
expect(decodeRefreshToken.role).toBe('none admin');
|
||||
expect(decodeRefreshToken.tier).toBe(TIERS.TIER5);
|
||||
expect(decodeRefreshToken.userId).toBe(partnerAdmin.external_id);
|
||||
expect(decodeRefreshToken.delegateUserId).toBe(parentAdmin.external_id);
|
||||
|
||||
const token = await service.updateDelegationAccessToken(
|
||||
context,
|
||||
decodeRefreshToken.delegateUserId,
|
||||
decodeRefreshToken.userId,
|
||||
delegationRefreshToken,
|
||||
);
|
||||
|
||||
// 取得できた代行操作用リフレッシュトークンをデコード
|
||||
const decodeAccessToken = decode<RefreshToken>(token);
|
||||
if (isVerifyError(decodeAccessToken)) {
|
||||
fail();
|
||||
}
|
||||
|
||||
expect(decodeAccessToken.role).toBe('none admin');
|
||||
expect(decodeAccessToken.tier).toBe(TIERS.TIER5);
|
||||
expect(decodeAccessToken.userId).toBe(partnerAdmin.external_id);
|
||||
expect(decodeAccessToken.delegateUserId).toBe(parentAdmin.external_id);
|
||||
});
|
||||
|
||||
it('代行操作対象アカウントの代行操作が許可されていない場合、エラーとなること', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
const service = module.get<AuthService>(AuthService);
|
||||
const { admin: parentAdmin, account: parentAccount } =
|
||||
await makeTestAccount(source, {
|
||||
tier: 4,
|
||||
});
|
||||
const { admin: partnerAdmin, account: partnerAccount } =
|
||||
await makeTestAccount(
|
||||
source,
|
||||
{
|
||||
tier: 5,
|
||||
parent_account_id: parentAccount.id,
|
||||
delegation_permission: true,
|
||||
},
|
||||
{ role: USER_ROLES.NONE },
|
||||
);
|
||||
|
||||
const context = makeContext(parentAdmin.external_id);
|
||||
|
||||
const delegationRefreshToken = await service.generateDelegationRefreshToken(
|
||||
context,
|
||||
parentAdmin.external_id,
|
||||
partnerAccount.id,
|
||||
);
|
||||
|
||||
// 取得できた代行操作用リフレッシュトークンをデコード
|
||||
const decodeRefreshToken = decode<RefreshToken>(delegationRefreshToken);
|
||||
if (isVerifyError(decodeRefreshToken)) {
|
||||
fail();
|
||||
}
|
||||
|
||||
expect(decodeRefreshToken.role).toBe('none admin');
|
||||
expect(decodeRefreshToken.tier).toBe(TIERS.TIER5);
|
||||
expect(decodeRefreshToken.userId).toBe(partnerAdmin.external_id);
|
||||
expect(decodeRefreshToken.delegateUserId).toBe(parentAdmin.external_id);
|
||||
|
||||
if (decodeRefreshToken.delegateUserId === undefined) {
|
||||
fail();
|
||||
}
|
||||
|
||||
// 代行操作対象アカウントの代行操作を許可しないように変更
|
||||
await updateAccountDelegationPermission(source, partnerAccount.id, false);
|
||||
const account = await getAccount(source, partnerAccount.id);
|
||||
|
||||
expect(account?.delegation_permission ?? true).toBeFalsy();
|
||||
|
||||
try {
|
||||
await service.updateDelegationAccessToken(
|
||||
context,
|
||||
decodeRefreshToken.delegateUserId,
|
||||
decodeRefreshToken.userId,
|
||||
delegationRefreshToken,
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.UNAUTHORIZED);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E010503'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
it('代行操作対象アカウントが存在しない場合、エラーとなること', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
const service = module.get<AuthService>(AuthService);
|
||||
const { admin: parentAdmin, account: parentAccount } =
|
||||
await makeTestAccount(source, {
|
||||
tier: 4,
|
||||
});
|
||||
const { admin: partnerAdmin, account: partnerAccount } =
|
||||
await makeTestAccount(
|
||||
source,
|
||||
{
|
||||
tier: 5,
|
||||
parent_account_id: parentAccount.id,
|
||||
delegation_permission: true,
|
||||
},
|
||||
{ role: USER_ROLES.NONE },
|
||||
);
|
||||
|
||||
const context = makeContext(parentAdmin.external_id);
|
||||
|
||||
const delegationRefreshToken = await service.generateDelegationRefreshToken(
|
||||
context,
|
||||
parentAdmin.external_id,
|
||||
partnerAccount.id,
|
||||
);
|
||||
|
||||
// 取得できた代行操作用リフレッシュトークンをデコード
|
||||
const decodeRefreshToken = decode<RefreshToken>(delegationRefreshToken);
|
||||
if (isVerifyError(decodeRefreshToken)) {
|
||||
fail();
|
||||
}
|
||||
|
||||
expect(decodeRefreshToken.role).toBe('none admin');
|
||||
expect(decodeRefreshToken.tier).toBe(TIERS.TIER5);
|
||||
expect(decodeRefreshToken.userId).toBe(partnerAdmin.external_id);
|
||||
expect(decodeRefreshToken.delegateUserId).toBe(parentAdmin.external_id);
|
||||
|
||||
if (decodeRefreshToken.delegateUserId === undefined) {
|
||||
fail();
|
||||
}
|
||||
|
||||
// 代行操作対象アカウントを削除
|
||||
deleteAccount(source, partnerAccount.id);
|
||||
|
||||
try {
|
||||
await service.updateDelegationAccessToken(
|
||||
context,
|
||||
decodeRefreshToken.delegateUserId,
|
||||
partnerAdmin.external_id,
|
||||
delegationRefreshToken,
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.UNAUTHORIZED);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E010501'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const idTokenPayload = {
|
||||
exp: 9000000000,
|
||||
nbf: 1000000000,
|
||||
|
||||
@ -25,8 +25,15 @@ import {
|
||||
AccountNotFoundError,
|
||||
AdminUserNotFoundError,
|
||||
} from '../../repositories/accounts/errors/types';
|
||||
import { DelegationNotAllowedError } from '../../repositories/users/errors/types';
|
||||
import { RoleUnexpectedError, TierUnexpectedError } from './errors/types';
|
||||
import {
|
||||
DelegationNotAllowedError,
|
||||
UserNotFoundError,
|
||||
} from '../../repositories/users/errors/types';
|
||||
import {
|
||||
InvalidTokenFormatError,
|
||||
RoleUnexpectedError,
|
||||
TierUnexpectedError,
|
||||
} from './errors/types';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@ -328,6 +335,116 @@ export class AuthService {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 代行操作用アクセストークンを更新する
|
||||
* @param context
|
||||
* @param delegateUserExternalId
|
||||
* @param originUserExternalId
|
||||
* @param refreshToken
|
||||
* @returns delegation access token
|
||||
*/
|
||||
async updateDelegationAccessToken(
|
||||
context: Context,
|
||||
delegateUserExternalId: string | undefined,
|
||||
originUserExternalId: string,
|
||||
refreshToken: string,
|
||||
): Promise<string> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.updateDelegationAccessToken.name} | params: { ` +
|
||||
`delegateUserExternalId: ${delegateUserExternalId}, ` +
|
||||
`originUserExternalId: ${originUserExternalId}, };`,
|
||||
);
|
||||
try {
|
||||
if (!delegateUserExternalId) {
|
||||
throw new UserNotFoundError('delegateUserExternalId is undefined');
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findUserByExternalId(
|
||||
delegateUserExternalId,
|
||||
);
|
||||
|
||||
const privateKey = getPrivateKey(this.configService);
|
||||
const pubkey = getPublicKey(this.configService);
|
||||
|
||||
// トークンの検証
|
||||
const decodedToken = verify<RefreshToken>(refreshToken, pubkey);
|
||||
if (isVerifyError(decodedToken)) {
|
||||
throw new InvalidTokenFormatError(
|
||||
`Invalid token format. ${decodedToken.reason} | ${decodedToken.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
// トークンの生成には検証済みトークンの値を使用する
|
||||
const { userId, delegateUserId, tier, role } = decodedToken;
|
||||
|
||||
if (delegateUserId === undefined) {
|
||||
throw new AdminUserNotFoundError('delegateUserId is undefined');
|
||||
}
|
||||
|
||||
// 代行操作対象アカウントの管理者ユーザーが存在して、アカウントに対して代行操作権限があるか確認
|
||||
const delegationPermission =
|
||||
await this.usersRepository.isAllowDelegationPermission(
|
||||
user.account_id,
|
||||
userId,
|
||||
);
|
||||
|
||||
if (!delegationPermission) {
|
||||
throw new DelegationNotAllowedError(
|
||||
`Delegation is not allowed. delegateUserId=${delegateUserId}, userId=${userId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const accessToken = sign<AccessToken>(
|
||||
{
|
||||
role: role,
|
||||
tier: tier,
|
||||
userId: userId,
|
||||
delegateUserId: delegateUserId,
|
||||
},
|
||||
this.accessTokenlifetime,
|
||||
privateKey,
|
||||
);
|
||||
|
||||
return accessToken;
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
if (e instanceof HttpException) {
|
||||
throw e;
|
||||
}
|
||||
if (e instanceof Error) {
|
||||
switch (e.constructor) {
|
||||
case InvalidTokenFormatError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
case UserNotFoundError:
|
||||
case AccountNotFoundError:
|
||||
case AdminUserNotFoundError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010501'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
case DelegationNotAllowedError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010503'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.updateDelegationAccessToken.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets id token
|
||||
* @param token
|
||||
|
||||
@ -2,3 +2,5 @@
|
||||
export class RoleUnexpectedError extends Error {}
|
||||
// Tier範囲想定外エラー
|
||||
export class TierUnexpectedError extends Error {}
|
||||
// トークン形式不正エラー
|
||||
export class InvalidTokenFormatError extends Error {}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Term } from '../../../repositories/terms/entity/term.entity';
|
||||
import { Account } from '../../../repositories/accounts/entity/account.entity';
|
||||
import { User } from '../../../repositories/users/entity/user.entity';
|
||||
|
||||
export const createTermInfo = async (
|
||||
datasource: DataSource,
|
||||
@ -16,3 +18,21 @@ export const createTermInfo = async (
|
||||
});
|
||||
identifiers.pop() as Term;
|
||||
};
|
||||
|
||||
export const updateAccountDelegationPermission = async (
|
||||
dataSource: DataSource,
|
||||
id: number,
|
||||
delegationPermission: boolean,
|
||||
): Promise<void> => {
|
||||
await dataSource
|
||||
.getRepository(Account)
|
||||
.update({ id: id }, { delegation_permission: delegationPermission });
|
||||
};
|
||||
|
||||
export const deleteAccount = async (
|
||||
dataSource: DataSource,
|
||||
id: number,
|
||||
): Promise<void> => {
|
||||
await dataSource.getRepository(User).delete({ account_id: id });
|
||||
await dataSource.getRepository(Account).delete({ id: id });
|
||||
};
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
|
||||
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
|
||||
import { AccessToken } from '../../common/token';
|
||||
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
|
||||
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
|
||||
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
|
||||
|
||||
@ -601,4 +601,47 @@ export class UsersRepositoryService {
|
||||
return primaryUser;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 代行操作対象のユーザーの所属するアカウントの代行操作が許可されているか
|
||||
* @param delegateAccountId 代行操作者のアカウントID
|
||||
* @param originAccountId 代行操作対象のアカウントID
|
||||
* @returns delegate accounts
|
||||
*/
|
||||
async isAllowDelegationPermission(
|
||||
delegateAccountId: number,
|
||||
originUserExternalId: string,
|
||||
): Promise<boolean> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const userRepo = entityManager.getRepository(User);
|
||||
const primaryUser = await userRepo.findOne({
|
||||
where: {
|
||||
external_id: originUserExternalId,
|
||||
account: {
|
||||
parent_account_id: delegateAccountId,
|
||||
tier: TIERS.TIER5,
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
account: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!primaryUser) {
|
||||
throw new AdminUserNotFoundError(
|
||||
`Admin user is not found. externalId: ${originUserExternalId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const originAccount = primaryUser.account;
|
||||
|
||||
// 運用上、アカウントがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||||
if (!originAccount) {
|
||||
throw new Error(`Account is Not Found. id: ${primaryUser.account_id}`);
|
||||
}
|
||||
|
||||
// 代行操作の許可の有無を返却
|
||||
return originAccount.delegation_permission;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user