Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions prisma/seed/credential-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,26 @@
* 통과 못 하는 값으로 만들어 두면 시드는 성공하는데 로그인은 ValidationPipe에서 전부 거절된다.
*/
import { IsStrongPasswordConstraint } from '@/common/validators/strong-password.validator';

const USERNAME_PATTERN = /^[a-z0-9._-]{4,80}$/;
import {
MAX_USERNAME_LENGTH,
MIN_USERNAME_LENGTH,
USERNAME_PATTERN,
} from '@/features/auth/constants/auth-admin.constants';

export function assertSeedCredential(args: {
username: string;
password: string;
}): void {
if (!USERNAME_PATTERN.test(args.username)) {
const { username } = args;
if (
username.length < MIN_USERNAME_LENGTH ||
username.length > MAX_USERNAME_LENGTH ||
!USERNAME_PATTERN.test(username)
) {
// 값은 메시지에 싣지 않는다 — env에서 온 자격증명이 오류 로그로 새는 경로(CodeQL clear-text-logging)
throw new Error('시드 username 정책 위반: 4~80자, 소문자·숫자·._- 만 허용');
throw new Error(
`시드 username 정책 위반: ${MIN_USERNAME_LENGTH}~${MAX_USERNAME_LENGTH}자, 영문 대소문자·숫자·._- 만 허용`,
);
}
if (!new IsStrongPasswordConstraint().validate(args.password)) {
throw new Error(
Expand Down
61 changes: 61 additions & 0 deletions src/test/seed-credential-policy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import 'reflect-metadata';

import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';

import { assertSeedCredential } from '../../prisma/seed/credential-policy';

import { AdminCreateAdminInput } from '@/features/auth/dto/inputs/admin-create-admin.input';

const PASSWORD = 'Strong!Pass1';

function seedAccepts(username: string): boolean {
try {
assertSeedCredential({ username, password: PASSWORD });
return true;
} catch {
return false;
}
}

async function dtoAccepts(username: string): Promise<boolean> {
const input = plainToInstance(AdminCreateAdminInput, {
username,
password: PASSWORD,
});
return (await validate(input)).length === 0;
}

// 시드가 만든 계정이 관리자 생성 경로와 같은 username 정책을 따르는지 — 두 판정이 어긋나면 안 된다
describe('시드 자격증명 username 정책', () => {
it.each([
['소문자', 'ops.admin', true],
['대문자 혼용', 'Ops.Admin_1', true],
['전부 대문자', 'OPS-ADMIN', true],
['4자', 'abcd', true],
['80자', 'a'.repeat(80), true],
['3자', 'abc', false],
['81자', 'a'.repeat(81), false],
['공백', 'ops admin', false],
['허용 외 문자(@)', 'ops@admin', false],
['한글', '관리자계정', false],
])('%s: 생성 DTO와 같은 판정(%s → %s)', async (_label, username, ok) => {
expect(seedAccepts(username)).toBe(ok);
expect(await dtoAccepts(username)).toBe(ok);
});

it('위반 메시지에 username 값을 싣지 않는다', () => {
expect(() =>
assertSeedCredential({ username: 'bad@name', password: PASSWORD }),
).toThrow(/^시드 username 정책 위반: 4~80자/);
expect(() =>
assertSeedCredential({ username: 'bad@name', password: PASSWORD }),
).not.toThrow(/bad@name/);
});

it('password 정책 위반은 별도 메시지로 거절한다', () => {
expect(() =>
assertSeedCredential({ username: 'ops.admin', password: 'weak' }),
).toThrow(/^시드 password 정책 위반/);
});
});
Loading