본문으로 건너뛰기

9.4 컬럼·뷰 권한

테이블 단위 권한(9.1)만으로는 모자란 경우가 있습니다. 어떤 사용자는 한 테이블의 일부 컬럼만 봐야 하거나, 가공·필터링한 결과만 보여야 합니다. 이럴 때 쓰는 것이 컬럼 단위 권한뷰(view) 권한, 그리고 함수 권한을 가르는 SECURITY DEFINER/INVOKER입니다.

컬럼 단위 권한

GRANT SELECT (col1, col2) ON table TO role 형식으로 특정 컬럼만 권한 부여.

-- users.email, users.phone 은 민감 — 일부 역할은 못 보게
GRANT SELECT (id, name) ON users TO app_readers;
-- email, phone에는 SELECT 권한 없음

확인:

SELECT column_name, privilege_type
  FROM information_schema.column_privileges
 WHERE table_name = 'users' AND grantee = 'app_readers';

INSERT·UPDATE도 컬럼 단위 가능:

GRANT INSERT (name, status) ON users TO app_writers;
-- email, phone에는 INSERT 권한 없음 (기본값 또는 NULL만)

함정

-- app_readers가 모든 컬럼을 SELECT 시도하면 ERROR
SELECT * FROM users;
-- ERROR: permission denied for column email

-- 명시적으로 허용된 컬럼만 SELECT 가능
SELECT id, name FROM users;
-- 정상

운영 코드의 SELECT *을 모두 명시 컬럼으로 바꿔야 하는 부담입니다. 그래서 컬럼 권한보다 가 더 흔히 쓰입니다.

뷰로 컬럼 제한

CREATE VIEW users_public AS
  SELECT id, name, created_at
  FROM users;

GRANT SELECT ON users_public TO app_readers;
REVOKE ALL ON users FROM app_readers;

뷰는 재작성 단계에서 underlying 쿼리로 풀립니다(5.1). 어떤 권한으로 underlying 테이블에 접근하는지가 핵심이고, 여기에는 두 가지 모드가 있습니다.

security_barrier

기본 뷰는 옵티마이저가 뷰 안과 바깥의 WHERE를 자유롭게 섞을 수 있습니다. 보안에 쓰는 뷰는 이게 위험합니다.

-- 정의: salary가 1억 미만인 사람만 보이는 뷰
CREATE VIEW employees_public AS
  SELECT id, name, salary
  FROM employees
  WHERE salary < 100000000;

-- 사용자가:
SELECT * FROM employees_public
 WHERE name = pg_terminate_backend(1234);   -- 부작용 있는 함수

옵티마이저가 사용자 WHERE를 underlying 쿼리에 먼저 평가하면 salary 조건 통과 전에 pg_terminate_backend(1234)가 호출돼 row가 leak되거나 부작용이 일어납니다.

해결: security_barrier:

CREATE VIEW employees_public WITH (security_barrier = true) AS
  SELECT id, name, salary
  FROM employees
  WHERE salary < 100000000;

옵티마이저가 뷰의 WHERE를 먼저 평가하도록 강제합니다. 보안용 뷰에는 항상 security_barrier = true를 붙입니다.

updatable view

단순 뷰는 INSERT/UPDATE/DELETE 가능 (auto-updatable). 조건:

  • 단일 테이블 FROM
  • 집계·GROUP BY·DISTINCT·LIMIT 없음
  • 모든 컬럼이 단순 컬럼 참조
CREATE VIEW active_users AS
  SELECT id, name, email FROM users WHERE deleted_at IS NULL;

-- 자동 updatable
INSERT INTO active_users (name, email) VALUES ('a','a@b.com');
-- users 에 deleted_at = NULL로 자동 삽입

복잡한 뷰는 INSTEAD OF 트리거로 직접 변환 로직 정의합니다.

WITH CHECK OPTION

뷰에 INSERT/UPDATE 시 뷰의 조건을 위반하는 row가 들어가지 않게 강제.

CREATE VIEW active_users AS
  SELECT id, name, email FROM users WHERE deleted_at IS NULL
  WITH CHECK OPTION;

-- 정상
INSERT INTO active_users (name, email) VALUES ('a','a@b.com');

-- ERROR — 조건을 우회하려는 시도
UPDATE active_users SET deleted_at = now() WHERE id = 1;
-- ERROR:  new row violates check option for view "active_users"

WITH CASCADED CHECK OPTION은 뷰 위에 또 뷰가 있을 때 모든 조건 체크.

SECURITY DEFINER vs INVOKER

함수 권한 컨텍스트의 두 모드.

옵션의미
SECURITY INVOKER (기본)호출자(현재 사용자)의 권한으로 함수 본문 실행
SECURITY DEFINER함수 생성자의 권한으로 본문 실행

DEFINER는 사용자가 직접 만질 수 없는 데이터를 정해진 함수로만 접근하게 하는 패턴입니다.

-- 일반 사용자는 audit_log 직접 INSERT 불가
REVOKE ALL ON audit_log FROM app_writers;

-- 함수로만 추가 가능
CREATE FUNCTION log_event(event text) RETURNS void AS $$
  INSERT INTO audit_log(event, created_at, user_name)
    VALUES (event, now(), session_user);
$$ LANGUAGE sql
SECURITY DEFINER
SET search_path = pg_catalog, public;   -- 보안 베스트 프랙티스

GRANT EXECUTE ON FUNCTION log_event(text) TO app_writers;

SECURITY DEFINER의 함정

함정대응
search_path가 사용자 제어 가능SET search_path = pg_catalog, ...로 고정
함수 본문이 사용자 입력을 SQL로 연결SQL injection 위험 — 파라미터화
잊혀진 SECURITY DEFINER 함수가 슈퍼유저 권한 노출정기 감사
-- 위험한 예
CREATE FUNCTION bad_lookup(t text) RETURNS setof record AS $$
  EXECUTE 'SELECT * FROM ' || t;   -- t를 그대로 SQL에 — SQL injection
$$ LANGUAGE plpgsql SECURITY DEFINER;

search_path와 보안

PostgreSQL은 객체 이름을 search_path 순서로 해석. 사용자가 search_path를 조작해 다른 스키마의 동명 객체로 함수가 흘러가게 만들 수 있습니다.

운영 베스트:

CREATE FUNCTION ...
  SET search_path = pg_catalog, public, app
  AS $$ ... $$;

함수 안에서 search_path를 명시적으로 고정. SECURITY DEFINER에서는 필수입니다.

컬럼·뷰 vs RLS 선택

요구사항권장
일부 컬럼 숨김뷰 (security_barrier=true) 또는 컬럼 권한
일부 row 숨김RLS
일부 row + 일부 컬럼뷰 (둘 다 포함)
사용자별 다른 데이터RLS
복잡한 가공·변환뷰 + WITH CHECK OPTION
정해진 함수로만 접근SECURITY DEFINER 함수

운영 안티패턴

안티패턴문제
보안용 뷰에 security_barrier 빠뜨림우회 가능
SECURITY DEFINER 함수가 search_path 미고정권한 escalation 가능
컬럼 권한과 뷰를 섞어 씀권한 모델 불분명
INSTEAD OF 트리거에 비즈니스 로직 폭주디버그·추적 어려움

정리

일부 컬럼만 노출할 때는 컬럼 권한(GRANT SELECT (col1, col2))이나 뷰를 쓰는데, SELECT * 부담이 적은 뷰 쪽이 실무에서 더 흔합니다. 보안용 뷰에는 security_barrier = true를 반드시 붙이고, INSERT/UPDATE가 뷰 조건을 벗어나지 못하게 하려면 WITH CHECK OPTION을 더합니다. 정해진 인터페이스로만 보호 데이터에 닿게 하려면 SECURITY DEFINER 함수를 쓰고, RLS와 결합하면 더 촘촘한 권한 모델이 됩니다.

  • 보안용 뷰: security_barrier = true 필수
  • SECURITY DEFINER 함수: 항상 search_path 명시 (권한 escalation 방어)