Files
vexplor/docs/채번규칙_멀티테넌시_버그_수정_완료.md
2025-11-06 17:01:13 +09:00

8.3 KiB

채번 규칙 멀티테넌시 버그 수정 완료

작성일: 2025-11-06
상태: 완료


🐛 문제 발견

증상

  • 다른 회사 계정으로 로그인했는데 company_code = "*" (최고 관리자 전용) 채번 규칙이 보임
  • 멀티테넌시 원칙 위반

원인

backend-node/src/services/numberingRuleService.ts의 SQL 쿼리에서 잘못된 WHERE 조건 사용:

// ❌ 잘못된 쿼리 (버그)
WHERE company_code = $1 OR company_code = '*'

문제점:

  • OR company_code = '*' 조건이 항상 최고 관리자 데이터를 포함시킴
  • 일반 회사 사용자도 company_code = "*" 데이터를 볼 수 있음
  • 멀티테넌시 보안 위반

수정 내용

수정된 로직

// ✅ 올바른 쿼리 (수정 후)
if (companyCode === "*") {
  // 최고 관리자: 모든 회사 데이터 조회 가능
  query = `SELECT * FROM numbering_rules`;
  params = [];
} else {
  // 일반 회사: 자신의 데이터만 조회 (company_code="*" 제외)
  query = `SELECT * FROM numbering_rules WHERE company_code = $1`;
  params = [companyCode];
}

수정된 메서드 목록

메서드 수정 내용 라인
getRuleList() 멀티테넌시 필터링 추가 40-150
getAvailableRulesForMenu() 멀티테넌시 필터링 추가 155-402
getRuleById() 멀티테넌시 필터링 추가 407-506

📊 수정 전후 비교

수정 전 (버그)

-- 일반 회사 (COMPANY_A) 로그인 시
SELECT * FROM numbering_rules 
WHERE company_code = 'COMPANY_A' OR company_code = '*';

-- 결과: 3건
-- 1. SAMPLE_RULE (company_code = '*')  ← 보면 안 됨!
-- 2. 사번코드 (company_code = '*')     ← 보면 안 됨!
-- 3. COMPANY_A 전용 규칙 (있다면)

수정 후 (정상)

-- 일반 회사 (COMPANY_A) 로그인 시
SELECT * FROM numbering_rules 
WHERE company_code = 'COMPANY_A';

-- 결과: 1건 (또는 0건)
-- 1. COMPANY_A 전용 규칙만 조회
-- company_code="*" 데이터는 제외됨!
-- 최고 관리자 (company_code = '*') 로그인 시
SELECT * FROM numbering_rules;

-- 결과: 모든 규칙 조회 가능
-- - SAMPLE_RULE (company_code = '*')
-- - 사번코드 (company_code = '*')
-- - COMPANY_A 전용 규칙
-- - COMPANY_B 전용 규칙
-- 등 모든 회사 데이터

🔍 상세 수정 내역

1. getRuleList() 메서드

Before:

const query = `
  SELECT * FROM numbering_rules
  WHERE company_code = $1 OR company_code = '*'
`;
const result = await pool.query(query, [companyCode]);

After:

let query: string;
let params: any[];

if (companyCode === "*") {
  // 최고 관리자: 모든 데이터 조회
  query = `SELECT * FROM numbering_rules ORDER BY created_at DESC`;
  params = [];
  logger.info("최고 관리자 전체 채번 규칙 조회");
} else {
  // 일반 회사: 자신의 데이터만 조회
  query = `SELECT * FROM numbering_rules WHERE company_code = $1 ORDER BY created_at DESC`;
  params = [companyCode];
  logger.info("회사별 채번 규칙 조회", { companyCode });
}

const result = await pool.query(query, params);

2. getAvailableRulesForMenu() 메서드

Before:

// menuObjid 없을 때
const query = `
  SELECT * FROM numbering_rules
  WHERE (company_code = $1 OR company_code = '*')
    AND scope_type = 'global'
`;

// menuObjid 있을 때
const query = `
  SELECT * FROM numbering_rules
  WHERE (company_code = $1 OR company_code = '*')
    AND (scope_type = 'global' OR (scope_type = 'menu' AND menu_objid = $2))
`;

After:

// 최고 관리자와 일반 회사를 명확히 구분
if (companyCode === "*") {
  // 최고 관리자 쿼리
  query = `SELECT * FROM numbering_rules WHERE scope_type = 'global'`;
} else {
  // 일반 회사 쿼리 (company_code="*" 제외)
  query = `SELECT * FROM numbering_rules WHERE company_code = $1 AND scope_type = 'global'`;
}

3. getRuleById() 메서드

Before:

const query = `
  SELECT * FROM numbering_rules
  WHERE rule_id = $1 AND (company_code = $2 OR company_code = '*')
`;
const result = await pool.query(query, [ruleId, companyCode]);

After:

if (companyCode === "*") {
  // 최고 관리자: rule_id만 체크
  query = `SELECT * FROM numbering_rules WHERE rule_id = $1`;
  params = [ruleId];
} else {
  // 일반 회사: rule_id + company_code 체크
  query = `SELECT * FROM numbering_rules WHERE rule_id = $1 AND company_code = $2`;
  params = [ruleId, companyCode];
}

🧪 테스트 시나리오

시나리오 1: 최고 관리자 로그인

# 로그인
POST /api/auth/login
{
  "userId": "admin",
  "password": "****"
}
# → JWT 토큰에 companyCode = "*" 포함

# 채번 규칙 조회
GET /api/numbering-rules
Authorization: Bearer {token}

# 예상 결과: 모든 회사의 규칙 조회 가능
[
  { "ruleId": "SAMPLE_RULE", "companyCode": "*" },
  { "ruleId": "사번코드", "companyCode": "*" },
  { "ruleId": "COMPANY_A_RULE", "companyCode": "COMPANY_A" },
  { "ruleId": "COMPANY_B_RULE", "companyCode": "COMPANY_B" }
]

시나리오 2: 일반 회사 (COMPANY_A) 로그인

# 로그인
POST /api/auth/login
{
  "userId": "user_a",
  "password": "****"
}
# → JWT 토큰에 companyCode = "COMPANY_A" 포함

# 채번 규칙 조회
GET /api/numbering-rules
Authorization: Bearer {token}

# 예상 결과: 자신의 회사 규칙만 조회 (company_code="*" 제외)
[
  { "ruleId": "COMPANY_A_RULE", "companyCode": "COMPANY_A" }
]

시나리오 3: 일반 회사 (COMPANY_B) 로그인

# 로그인
POST /api/auth/login
{
  "userId": "user_b",
  "password": "****"
}
# → JWT 토큰에 companyCode = "COMPANY_B" 포함

# 채번 규칙 조회
GET /api/numbering-rules
Authorization: Bearer {token}

# 예상 결과: COMPANY_B 규칙만 조회
[
  { "ruleId": "COMPANY_B_RULE", "companyCode": "COMPANY_B" }
]

🎯 멀티테넌시 원칙 재확인

핵심 원칙

company_code = "*"는 최고 관리자 전용 데이터이며, 일반 회사는 절대 접근할 수 없습니다.

회사 코드 조회 가능 데이터 설명
* (최고 관리자) 모든 회사 데이터 company_code = "*", "COMPANY_A", "COMPANY_B" 등 모두 조회
COMPANY_A COMPANY_A 데이터만 company_code = "*" 데이터는 절대 조회 불가
COMPANY_B COMPANY_B 데이터만 company_code = "*" 데이터는 절대 조회 불가

SQL 패턴

-- ❌ 잘못된 패턴 (버그)
WHERE company_code = $1 OR company_code = '*'

-- ✅ 올바른 패턴 (최고 관리자)
WHERE 1=1  -- 모든 데이터

-- ✅ 올바른 패턴 (일반 회사)
WHERE company_code = $1  -- company_code="*" 자동 제외

📝 추가 확인 사항

다른 서비스에도 같은 버그가 있을 가능성

다음 서비스들도 동일한 패턴으로 멀티테넌시 버그가 있는지 확인 필요:

  • backend-node/src/services/screenService.ts
  • backend-node/src/services/tableService.ts
  • backend-node/src/services/flowService.ts
  • backend-node/src/services/adminService.ts
  • 기타 company_code 필터링을 사용하는 모든 서비스

확인 방법

# 잘못된 패턴 검색
cd backend-node/src/services
grep -n "OR company_code = '\*'" *.ts

🚀 배포 전 체크리스트

  • 코드 수정 완료
  • 린트 에러 없음
  • 로깅 추가 (최고 관리자 vs 일반 회사 구분)
  • 단위 테스트 작성 (선택)
  • 통합 테스트 (필수)
    • 최고 관리자로 로그인하여 모든 규칙 조회 확인
    • 일반 회사로 로그인하여 자신의 규칙만 조회 확인
    • 다른 회사 규칙에 접근 불가능 확인
  • 프론트엔드에서 채번 규칙 목록 재확인
  • 백엔드 재실행 (코드 변경 사항 반영)

📚 관련 문서


수정 완료일: 2025-11-06
수정자: AI Assistant
영향 범위: numberingRuleService.ts 전체