ERP-node/docs/리소스_기반_권한_시스템_가이드.md

13 KiB

리소스 기반 권한 시스템 가이드

개요

동적으로 화면과 테이블을 생성하는 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_idNULL이면 해당 타입 전체에 대한 권한

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_userresource_permissions)

    • 사용자가 속한 권한 그룹의 권한
  4. 개별 권한 (user_resource_permissions)

    • 사용자에게 직접 부여된 권한

최종 판정: 권한 그룹 권한 OR 개별 권한 (하나라도 TRUE이면 허용)


사용 예시

예시 1: 영업팀에게 모든 화면 읽기 권한 부여

-- 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: 특정 화면에만 수정 권한 부여

-- 특정 화면 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 권한 부여 (삭제 제외)

-- 모든 테이블에 대해 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: 플로우 실행 권한 부여

-- 특정 플로우만 실행 가능
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: 개별 사용자에게 직접 권한 부여

-- '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. 권한 체크 함수

-- 사용자 '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. 접근 가능한 리소스 목록 조회

-- 사용자 '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 예시

// 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 };
}

컴포넌트에서 사용

// 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 전체
  • 영업 플로우만 실행 가능
  • 데이터 내보내기 가능
-- 영업팀 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: 읽기 전용 사용자

요구사항:

  • 모든 리소스 읽기만 가능
  • 수정/삭제/생성 불가
-- 읽기 전용 권한 그룹 생성
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
  • 플로우 생성/실행
-- 개발팀 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');

마이그레이션 실행

# 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. 시간 기반 권한

ALTER TABLE resource_permissions ADD COLUMN valid_from TIMESTAMP;
ALTER TABLE resource_permissions ADD COLUMN valid_until TIMESTAMP;

2. 조건부 권한 (Row-Level Security)

-- 예: 자신이 생성한 데이터만 수정 가능
ALTER TABLE resource_permissions ADD COLUMN row_condition TEXT;
-- 'created_by = :user_id'

3. 권한 요청/승인 워크플로우

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