417 lines
13 KiB
Markdown
417 lines
13 KiB
Markdown
# 리소스 기반 권한 시스템 가이드
|
|
|
|
## 개요
|
|
|
|
동적으로 화면과 테이블을 생성하는 Low-Code 플랫폼에 맞춘 **리소스 기반 권한 시스템**입니다.
|
|
|
|
전통적인 "메뉴" 개념 대신, **"리소스 타입"**(화면, 테이블, 플로우 등)에 대한 **세밀한 CRUD 권한**을 관리합니다.
|
|
|
|
## 왜 메뉴 기반이 아닌가?
|
|
|
|
### 문제점
|
|
|
|
- 현재 시스템은 **동적으로 화면(`screen_definitions`)을 생성**
|
|
- 사용자가 **DDL을 실행하여 테이블을 동적으로 생성**
|
|
- **메뉴는 고정되어 있지 않음** (사용자가 생성한 화면 = 새로운 "메뉴")
|
|
|
|
### 해결책
|
|
|
|
- **리소스 타입** (SCREEN, TABLE, FLOW, DASHBOARD 등) 기반 권한
|
|
- **특정 리소스 ID** 또는 **전체 타입**에 대한 권한 부여
|
|
- **6가지 세밀한 권한**: Create, Read, Update, Delete, Execute, Export
|
|
|
|
---
|
|
|
|
## 시스템 구조
|
|
|
|
### 1. 리소스 타입 (`resource_types`)
|
|
|
|
| type_code | type_name | description |
|
|
| --------- | --------- | ------------------------------ |
|
|
| SCREEN | 화면 | 동적으로 생성된 화면 |
|
|
| TABLE | 테이블 | 동적으로 생성된 데이터 테이블 |
|
|
| FLOW | 플로우 | 데이터 플로우 |
|
|
| DASHBOARD | 대시보드 | 대시보드 |
|
|
| REPORT | 리포트 | 리포트 |
|
|
| API | API | 외부 API 호출 |
|
|
| FILE | 파일 | 파일 업로드/다운로드 |
|
|
| SYSTEM | 시스템 | 시스템 설정 (SUPER_ADMIN 전용) |
|
|
|
|
### 2. 권한 그룹 (`authority_master`)
|
|
|
|
기존 테이블 활용 (회사별 격리 지원):
|
|
|
|
- `objid`: 권한 그룹 ID
|
|
- `auth_name`: 권한 그룹 이름 (예: "영업팀", "개발팀")
|
|
- `auth_code`: 권한 그룹 코드
|
|
- `company_code`: 회사 코드
|
|
- `status`: 활성/비활성
|
|
|
|
### 3. 리소스별 권한 (`resource_permissions`)
|
|
|
|
| 컬럼 | 타입 | 설명 |
|
|
| ------------- | ------------ | --------------------------------- |
|
|
| role_group_id | INTEGER | 권한 그룹 ID (FK) |
|
|
| resource_type | VARCHAR(50) | 리소스 타입 (SCREEN, TABLE 등) |
|
|
| resource_id | VARCHAR(255) | 특정 리소스 ID (**NULL = 전체**) |
|
|
| can_create | BOOLEAN | 생성 권한 |
|
|
| can_read | BOOLEAN | 읽기 권한 |
|
|
| can_update | BOOLEAN | 수정 권한 |
|
|
| can_delete | BOOLEAN | 삭제 권한 |
|
|
| can_execute | BOOLEAN | 실행 권한 (플로우 실행, DDL 실행) |
|
|
| can_export | BOOLEAN | 내보내기 권한 |
|
|
|
|
**핵심**: `resource_id`가 **NULL**이면 해당 타입 **전체**에 대한 권한
|
|
|
|
### 4. 사용자별 직접 권한 (`user_resource_permissions`)
|
|
|
|
권한 그룹 외에 **개별 사용자에게 직접 권한** 부여 가능 (보조적 사용)
|
|
|
|
---
|
|
|
|
## 권한 체크 로직
|
|
|
|
### 우선순위
|
|
|
|
1. **SUPER_ADMIN** (`company_code = '*'`, `user_type = 'SUPER_ADMIN'`)
|
|
|
|
- 모든 권한 (무조건 TRUE)
|
|
|
|
2. **COMPANY_ADMIN** (`user_type = 'COMPANY_ADMIN'`)
|
|
|
|
- 자기 회사 모든 리소스 권한 (단, `SYSTEM` 타입 제외)
|
|
|
|
3. **권한 그룹 기반 권한** (`authority_sub_user` → `resource_permissions`)
|
|
|
|
- 사용자가 속한 권한 그룹의 권한
|
|
|
|
4. **개별 권한** (`user_resource_permissions`)
|
|
- 사용자에게 직접 부여된 권한
|
|
|
|
**최종 판정**: `권한 그룹 권한 OR 개별 권한` (하나라도 TRUE이면 허용)
|
|
|
|
---
|
|
|
|
## 사용 예시
|
|
|
|
### 예시 1: 영업팀에게 모든 화면 읽기 권한 부여
|
|
|
|
```sql
|
|
-- 1. 영업팀 권한 그룹 ID 조회
|
|
SELECT objid FROM authority_master
|
|
WHERE auth_code = 'SALES_TEAM' AND company_code = 'ILSHIN';
|
|
-- 결과: objid = 1001
|
|
|
|
-- 2. 화면(SCREEN) 전체에 대한 읽기 권한 부여
|
|
INSERT INTO resource_permissions (role_group_id, resource_type, resource_id, can_read, created_by)
|
|
VALUES (1001, 'SCREEN', NULL, TRUE, 'admin');
|
|
-- ^^^^ ^^^^ NULL = 모든 화면
|
|
```
|
|
|
|
### 예시 2: 특정 화면에만 수정 권한 부여
|
|
|
|
```sql
|
|
-- 특정 화면 ID: 'SCR_SALES_REPORT' (screen_definitions.screen_code)
|
|
INSERT INTO resource_permissions (role_group_id, resource_type, resource_id, can_read, can_update, created_by)
|
|
VALUES (1001, 'SCREEN', 'SCR_SALES_REPORT', TRUE, TRUE, 'admin');
|
|
-- ^^^^^^^^^^^^^^^^^ 특정 화면만
|
|
```
|
|
|
|
### 예시 3: 테이블 CRUD 권한 부여 (삭제 제외)
|
|
|
|
```sql
|
|
-- 모든 테이블에 대해 CRU (Create, Read, Update) 권한 부여
|
|
INSERT INTO resource_permissions (
|
|
role_group_id, resource_type, resource_id,
|
|
can_create, can_read, can_update, can_delete,
|
|
created_by
|
|
)
|
|
VALUES (1001, 'TABLE', NULL, TRUE, TRUE, TRUE, FALSE, 'admin');
|
|
```
|
|
|
|
### 예시 4: 플로우 실행 권한 부여
|
|
|
|
```sql
|
|
-- 특정 플로우만 실행 가능
|
|
INSERT INTO resource_permissions (
|
|
role_group_id, resource_type, resource_id,
|
|
can_read, can_execute,
|
|
created_by
|
|
)
|
|
VALUES (1001, 'FLOW', '29', TRUE, TRUE, 'admin');
|
|
-- ^^ flow_definition.id
|
|
```
|
|
|
|
### 예시 5: 개별 사용자에게 직접 권한 부여
|
|
|
|
```sql
|
|
-- 'john.doe' 사용자에게 시스템 설정 읽기 권한
|
|
INSERT INTO user_resource_permissions (
|
|
user_id, resource_type, resource_id, can_read, created_by
|
|
)
|
|
VALUES ('john.doe', 'SYSTEM', NULL, TRUE, 'admin');
|
|
```
|
|
|
|
---
|
|
|
|
## 백엔드 API 사용법
|
|
|
|
### 1. 권한 체크 함수
|
|
|
|
```sql
|
|
-- 사용자 'john.doe'가 화면 'SCR_SALES_REPORT'를 읽을 수 있는지 확인
|
|
SELECT check_user_resource_permission('john.doe', 'SCREEN', 'SCR_SALES_REPORT', 'read');
|
|
-- 결과: TRUE 또는 FALSE
|
|
|
|
-- 테이블 'contract_mgmt'를 삭제할 수 있는지 확인
|
|
SELECT check_user_resource_permission('john.doe', 'TABLE', 'contract_mgmt', 'delete');
|
|
```
|
|
|
|
### 2. 접근 가능한 리소스 목록 조회
|
|
|
|
```sql
|
|
-- 사용자 'john.doe'가 읽을 수 있는 모든 화면 목록
|
|
SELECT * FROM get_user_accessible_resources('john.doe', 'SCREEN', 'read');
|
|
|
|
-- 결과 예시:
|
|
-- resource_id | can_create | can_read | can_update | can_delete | can_execute | can_export
|
|
-- ------------+------------+----------+------------+------------+-------------+-----------
|
|
-- * | FALSE | TRUE | FALSE | FALSE | FALSE | FALSE
|
|
-- SCR_SALES | FALSE | TRUE | TRUE | FALSE | FALSE | TRUE
|
|
```
|
|
|
|
---
|
|
|
|
## 프론트엔드 통합
|
|
|
|
### React Hook 예시
|
|
|
|
```typescript
|
|
// hooks/usePermission.ts
|
|
import { useState, useEffect } from "react";
|
|
import { checkResourcePermission } from "@/lib/api/permission";
|
|
|
|
export function usePermission(
|
|
resourceType: string,
|
|
resourceId: string | null,
|
|
permissionType: "create" | "read" | "update" | "delete" | "execute" | "export"
|
|
) {
|
|
const [hasPermission, setHasPermission] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const checkPermission = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const response = await checkResourcePermission({
|
|
resourceType,
|
|
resourceId,
|
|
permissionType,
|
|
});
|
|
setHasPermission(response.success && response.data?.hasPermission);
|
|
} catch (error) {
|
|
console.error("권한 확인 오류:", error);
|
|
setHasPermission(false);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
checkPermission();
|
|
}, [resourceType, resourceId, permissionType]);
|
|
|
|
return { hasPermission, isLoading };
|
|
}
|
|
```
|
|
|
|
### 컴포넌트에서 사용
|
|
|
|
```tsx
|
|
// components/ScreenDetail.tsx
|
|
import { usePermission } from "@/hooks/usePermission";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
export function ScreenDetail({ screenCode }: { screenCode: string }) {
|
|
const { hasPermission: canUpdate } = usePermission(
|
|
"SCREEN",
|
|
screenCode,
|
|
"update"
|
|
);
|
|
const { hasPermission: canDelete } = usePermission(
|
|
"SCREEN",
|
|
screenCode,
|
|
"delete"
|
|
);
|
|
|
|
return (
|
|
<div>
|
|
<h1>{screenCode}</h1>
|
|
{canUpdate && <Button>수정</Button>}
|
|
{canDelete && <Button variant="destructive">삭제</Button>}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 실전 시나리오
|
|
|
|
### 시나리오 1: 영업팀 권한 설정
|
|
|
|
**요구사항**:
|
|
|
|
- 모든 화면 조회 가능
|
|
- 계약 테이블(`contract_mgmt`) CRUD 전체
|
|
- 영업 플로우만 실행 가능
|
|
- 데이터 내보내기 가능
|
|
|
|
```sql
|
|
-- 영업팀 ID: 1001
|
|
INSERT INTO resource_permissions (role_group_id, resource_type, resource_id, can_create, can_read, can_update, can_delete, can_execute, can_export, created_by)
|
|
VALUES
|
|
-- 모든 화면 읽기
|
|
(1001, 'SCREEN', NULL, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, 'admin'),
|
|
-- 계약 테이블 CRUD
|
|
(1001, 'TABLE', 'contract_mgmt', TRUE, TRUE, TRUE, TRUE, FALSE, TRUE, 'admin'),
|
|
-- 영업 플로우 실행
|
|
(1001, 'FLOW', 'sales_flow', FALSE, TRUE, FALSE, FALSE, TRUE, FALSE, 'admin');
|
|
```
|
|
|
|
### 시나리오 2: 읽기 전용 사용자
|
|
|
|
**요구사항**:
|
|
|
|
- 모든 리소스 읽기만 가능
|
|
- 수정/삭제/생성 불가
|
|
|
|
```sql
|
|
-- 읽기 전용 권한 그룹 생성
|
|
INSERT INTO authority_master (objid, auth_name, auth_code, company_code, status, writer, regdate)
|
|
VALUES (nextval('seq_authority_master'), '읽기 전용', 'READ_ONLY', 'ILSHIN', 'active', 'admin', NOW());
|
|
|
|
-- 권한 부여
|
|
INSERT INTO resource_permissions (role_group_id, resource_type, resource_id, can_read, created_by)
|
|
SELECT
|
|
(SELECT objid FROM authority_master WHERE auth_code = 'READ_ONLY' AND company_code = 'ILSHIN'),
|
|
type_code,
|
|
NULL,
|
|
TRUE,
|
|
'admin'
|
|
FROM resource_types
|
|
WHERE type_code != 'SYSTEM'; -- 시스템 제외
|
|
```
|
|
|
|
### 시나리오 3: 개발팀 (DDL 실행 권한)
|
|
|
|
**요구사항**:
|
|
|
|
- 테이블 생성/삭제 가능 (DDL 실행)
|
|
- 모든 화면 CRUD
|
|
- 플로우 생성/실행
|
|
|
|
```sql
|
|
-- 개발팀 ID: 1002
|
|
INSERT INTO resource_permissions (role_group_id, resource_type, resource_id, can_create, can_read, can_update, can_delete, can_execute, created_by)
|
|
VALUES
|
|
-- 화면 CRUD
|
|
(1002, 'SCREEN', NULL, TRUE, TRUE, TRUE, TRUE, FALSE, 'admin'),
|
|
-- 테이블 CRUD + 실행(DDL)
|
|
(1002, 'TABLE', NULL, TRUE, TRUE, TRUE, TRUE, TRUE, 'admin'),
|
|
-- 플로우 CRUD + 실행
|
|
(1002, 'FLOW', NULL, TRUE, TRUE, TRUE, TRUE, TRUE, 'admin');
|
|
```
|
|
|
|
---
|
|
|
|
## 마이그레이션 실행
|
|
|
|
```bash
|
|
# Docker Compose 환경
|
|
docker exec -i <DB_CONTAINER_NAME> psql -U postgres -d ilshin < db/migrations/028_add_company_code_to_authority_master.sql
|
|
docker exec -i <DB_CONTAINER_NAME> psql -U postgres -d ilshin < db/migrations/029_create_resource_based_permission_system.sql
|
|
|
|
# 검증
|
|
docker exec -it <DB_CONTAINER_NAME> psql -U postgres -d ilshin -c "SELECT * FROM resource_types;"
|
|
docker exec -it <DB_CONTAINER_NAME> psql -U postgres -d ilshin -c "SELECT * FROM v_role_permissions_summary;"
|
|
```
|
|
|
|
---
|
|
|
|
## 추가 기능 확장 아이디어
|
|
|
|
### 1. 시간 기반 권한
|
|
|
|
```sql
|
|
ALTER TABLE resource_permissions ADD COLUMN valid_from TIMESTAMP;
|
|
ALTER TABLE resource_permissions ADD COLUMN valid_until TIMESTAMP;
|
|
```
|
|
|
|
### 2. 조건부 권한 (Row-Level Security)
|
|
|
|
```sql
|
|
-- 예: 자신이 생성한 데이터만 수정 가능
|
|
ALTER TABLE resource_permissions ADD COLUMN row_condition TEXT;
|
|
-- 'created_by = :user_id'
|
|
```
|
|
|
|
### 3. 권한 요청/승인 워크플로우
|
|
|
|
```sql
|
|
CREATE TABLE permission_requests (
|
|
request_id SERIAL PRIMARY KEY,
|
|
user_id VARCHAR(50),
|
|
resource_type VARCHAR(50),
|
|
resource_id VARCHAR(255),
|
|
permission_type VARCHAR(20),
|
|
reason TEXT,
|
|
status VARCHAR(20), -- 'pending', 'approved', 'rejected'
|
|
approved_by VARCHAR(50),
|
|
approved_date TIMESTAMP
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## FAQ
|
|
|
|
### Q1: 메뉴 기반 권한과 무엇이 다른가요?
|
|
|
|
**A**: 메뉴는 고정된 화면을 가정하지만, 이 시스템은 사용자가 **동적으로 생성한 화면/테이블**에도 권한을 부여할 수 있습니다. 예를 들어, 사용자 A가 "계약 관리" 화면을 생성하면, 권한 그룹 B에게 그 화면의 읽기 권한을 즉시 부여할 수 있습니다.
|
|
|
|
### Q2: `resource_id`가 NULL인 경우와 특정 ID인 경우의 차이는?
|
|
|
|
**A**:
|
|
|
|
- `resource_id = NULL`: **해당 타입의 모든 리소스**에 대한 권한
|
|
- `resource_id = 'SCR_001'`: **특정 리소스만** 권한
|
|
|
|
예: `(SCREEN, NULL, read)` = 모든 화면 읽기
|
|
예: `(SCREEN, 'SCR_001', read)` = SCR_001 화면만 읽기
|
|
|
|
### Q3: 권한 그룹과 개별 권한의 우선순위는?
|
|
|
|
**A**: **OR 연산**입니다. 권한 그룹에서 허용되거나, 개별 권한에서 허용되면 최종적으로 허용됩니다.
|
|
|
|
### Q4: COMPANY_ADMIN은 왜 SYSTEM 타입 권한이 없나요?
|
|
|
|
**A**: SYSTEM 타입은 **시스템 전체 설정**(예: 회사 생성/삭제, 전체 사용자 관리)이므로 SUPER_ADMIN만 접근 가능합니다.
|
|
|
|
### Q5: 동적으로 생성된 화면의 `resource_id`는 무엇인가요?
|
|
|
|
**A**: `screen_definitions.screen_code`를 사용합니다. 예: `'SCR_CONTRACT_MGMT'`
|
|
|
|
### Q6: 플로우의 `resource_id`는?
|
|
|
|
**A**: `flow_definition.id` (숫자)를 문자열로 변환하여 사용합니다. 예: `'29'`
|
|
|
|
---
|
|
|
|
## 관련 파일
|
|
|
|
- **마이그레이션**: `db/migrations/028_add_company_code_to_authority_master.sql`
|
|
- **마이그레이션**: `db/migrations/029_create_resource_based_permission_system.sql`
|
|
- **백엔드 서비스**: `backend-node/src/services/RoleService.ts`
|
|
- **프론트엔드 API**: `frontend/lib/api/role.ts`
|
|
- **권한 체계 가이드**: `docs/권한_체계_가이드.md`
|