Merged PR 93: [WIP]API実装

## 概要
[Task1630: API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1630)

- アップロード先取得APIの実行権限をチェックするためにアクセストークンに権限情報とロール情報を追加する
- ログイン時に発行しているトークンにパラメータを追加
  - role
    - ユーザーのrole情報(typist/author)
  - scope
    - 管理者権限があるか (admin/空文字)

- トークン発行前にDBからユーザーの情報を取得する処理を追加
- ユーザーを取得するときにユーザーが属しているアカウントの情報も取得するようにentitiyを修正
- `findUserByExternalId`で実行されるSQL
```
SELECT
    `User`.`id` AS `User_id`,
    `User`.`external_id` AS `User_external_id`,
    `User`.`account_id` AS `User_account_id`,
    `User`.`role` AS `User_role`,
    `User`.`author_id` AS `User_author_id`,
    `User`.`accepted_terms_version` AS `User_accepted_terms_version`,
    `User`.`email_verified` AS `User_email_verified`,
    `User`.`deleted_at` AS `User_deleted_at`,
    `User`.`created_by` AS `User_created_by`,
    `User`.`created_at` AS `User_created_at`,
    `User`.`updated_by` AS `User_updated_by`,
    `User`.`updated_at` AS `User_updated_at`,
    `User__User_account`.`id` AS `User__User_account_id`,
    `User__User_account`.`parent_account_id` AS `User__User_account_parent_account_id`,
    `User__User_account`.`tier` AS `User__User_account_tier`,
    `User__User_account`.`country` AS `User__User_account_country`,
    `User__User_account`.`delegation_permission` AS `User__User_account_delegation_permission`,
    `User__User_account`.`locked` AS `User__User_account_locked`,
    `User__User_account`.`company_name` AS `User__User_account_company_name`,
    `User__User_account`.`verified` AS `User__User_account_verified`,
    `User__User_account`.`primary_admin_user_id` AS `User__User_account_primary_admin_user_id`,
    `User__User_account`.`secondary_admin_user_id` AS `User__User_account_secondary_admin_user_id`,
    `User__User_account`.`deleted_at` AS `User__User_account_deleted_at`,
    `User__User_account`.`created_by` AS `User__User_account_created_by`,
    `User__User_account`.`created_at` AS `User__User_account_created_at`,
    `User__User_account`.`updated_by` AS `User__User_account_updated_by`,
    `User__User_account`.`updated_at` AS `User__User_account_updated_at`
FROM
    `users` `User`
    LEFT JOIN `accounts` `User__User_account` ON `User__User_account`.`id` = `User`.`account_id`
WHERE
    ((`User`.`external_id` = ?))
    AND (`User`.`id` IN (?)) -- PARAMETERS: ["B2CのID","2"]

```

## レビューポイント
- 管理者権限の有無とロールは別の概念であるため、別のパラメータとして用意したが問題なさそうか
  - 他の案としてscopeの中に`typist , admin`のようにして、一つのパラメータで権限チェックする?
- DBから取得するデータとしてユーザーが属しているアカウント情報のすべてのカラムを取得するようにしているが、必要なカラムのみにしたほうが良いか?

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- 実行SQLを確認、JWTの内容を確認

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
saito.k 2023-05-12 04:48:09 +00:00
parent 3f7a9ed11a
commit b91d260015
8 changed files with 63 additions and 15 deletions

View File

@ -2,7 +2,7 @@
// TODO トークンの型は仮
export interface Token {
userId: string;
scope: string;
role: string;
exp: number;
iat: number;
}
@ -13,7 +13,7 @@ export const isToken = (arg: any): arg is Token => {
if (arg.userId === undefined) {
return false;
}
if (arg.scope === undefined) {
if (arg.role === undefined) {
return false;
}
if (arg.exp === undefined) {

View File

@ -1,11 +1,11 @@
export type RefreshToken = {
userId: string;
scope: string;
role: string;
};
export type AccessToken = {
userId: string;
scope: string;
role: string;
};
export type IDToken = {

View File

@ -12,6 +12,7 @@ import {
} from '../../common/token';
import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
import { CryptoService } from '../../gateways/crypto/crypto.service';
import { User } from '../../repositories/users/entity/user.entity';
@Injectable()
export class AuthService {
@ -56,6 +57,20 @@ export class AuthService {
const lifetimeDefault = this.configService.get(
'REFRESH_TOKEN_LIFETIME_DEFAULT',
);
let user: User;
// ユーザー情報とユーザーが属しているアカウント情報を取得
try {
user = await this.usersRepository.findUserByExternalId(idToken.sub);
if (!user.account) {
throw new Error('Account information not found');
}
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
// 要求された環境用トークンの寿命を決定
const refreshTokenLifetime = type === 'web' ? lifetimeWeb : lifetimeDefault;
@ -63,7 +78,12 @@ export class AuthService {
const privateKey = await this.cryptoService.getPrivateKey();
const token = sign<RefreshToken>(
{
scope: 'test',
role: `${user.role} ${
user.account.primary_admin_user_id === user.id ||
user.account.secondary_admin_user_id === user.id
? 'admin'
: 'standard'
}`,
userId: idToken.sub,
},
refreshTokenLifetime,
@ -90,7 +110,7 @@ export class AuthService {
const accessToken = sign<AccessToken>(
{
scope: token.scope,
role: token.role,
userId: token.userId,
},
lifetime,

View File

@ -129,7 +129,7 @@ export class UsersController {
}
// アクセストークンの権限不足エラー
if (!confirmPermission(payload.scope)) {
if (!confirmPermission(payload.role)) {
throw new HttpException(
makeErrorResponse('E000108'),
HttpStatus.UNAUTHORIZED,
@ -199,7 +199,7 @@ export class UsersController {
);
}
//アクセストークンの権限不足エラー
if (!confirmPermission(payload.scope)) {
if (!confirmPermission(payload.role)) {
throw new HttpException(
makeErrorResponse('E000108'),
HttpStatus.UNAUTHORIZED,

View File

@ -259,7 +259,7 @@ describe('UsersService', () => {
const autoRenew = true;
const licenseAlert = true;
const notification = true;
const token: AccessToken = { userId: '0001', scope: '' };
const token: AccessToken = { userId: '0001', role: '' };
expect(
await service.createUser(
token,
@ -295,7 +295,7 @@ describe('UsersService', () => {
const licenseAlert = true;
const notification = true;
const authorId = 'testID';
const token: AccessToken = { userId: '0001', scope: '' };
const token: AccessToken = { userId: '0001', role: '' };
expect(
await service.createUser(
token,
@ -332,7 +332,7 @@ describe('UsersService', () => {
const licenseAlert = true;
const notification = true;
const typistGroupId = 111;
const token: AccessToken = { userId: '0001', scope: '' };
const token: AccessToken = { userId: '0001', role: '' };
expect(
await service.createUser(
token,
@ -369,7 +369,7 @@ it('DBネットワークエラーとなる場合、エラーとなる。', async
const autoRenew = true;
const licenseAlert = true;
const notification = true;
const token: AccessToken = { userId: '0001', scope: '' };
const token: AccessToken = { userId: '0001', role: '' };
await expect(
service.createUser(
token,
@ -407,7 +407,7 @@ it('Azure ADB2Cでネットワークエラーとなる場合、エラーとな
const autoRenew = true;
const licenseAlert = true;
const notification = true;
const token: AccessToken = { userId: '0001', scope: '' };
const token: AccessToken = { userId: '0001', role: '' };
await expect(
service.createUser(
token,
@ -445,7 +445,7 @@ it('メールアドレスが重複している場合、エラーとなる。', a
const autoRenew = true;
const licenseAlert = true;
const notification = true;
const token: AccessToken = { userId: '0001', scope: '' };
const token: AccessToken = { userId: '0001', role: '' };
await expect(
service.createUser(
token,
@ -483,7 +483,7 @@ it('AuthorIDが重複している場合、エラーとなる。', async () => {
const licenseAlert = true;
const notification = true;
const authorId = 'testID';
const token: AccessToken = { userId: '0001', scope: '' };
const token: AccessToken = { userId: '0001', role: '' };
await expect(
service.createUser(
token,

View File

@ -1,9 +1,11 @@
import { User } from '../../../repositories/users/entity/user.entity';
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
} from 'typeorm';
@Entity({ name: 'accounts' })
@ -52,4 +54,7 @@ export class Account {
@UpdateDateColumn()
updated_at: Date;
@OneToMany(() => User, (user) => user.id)
user?: User[];
}

View File

@ -1,9 +1,12 @@
import { Account } from '../../../repositories/accounts/entity/account.entity';
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
@Entity({ name: 'users' })
@ -52,4 +55,8 @@ export class User {
@UpdateDateColumn()
updated_at: Date;
@ManyToOne(() => Account, (account) => account.user)
@JoinColumn({ name: 'account_id' })
account?: Account;
}

View File

@ -93,6 +93,22 @@ export class UsersRepositoryService {
return user;
}
async findUserByExternalId(sub: string): Promise<User | undefined> {
const user = await this.dataSource.getRepository(User).findOne({
where: {
external_id: sub,
},
relations: {
account: true,
},
});
if (!user) {
return undefined;
}
return user;
}
async findUserById(id: number): Promise<User | undefined> {
const user = await this.dataSource.getRepository(User).findOne({
where: {