CREATE MASKING POLICY

CREATE MASKING POLICY

현재/지정한 스키마에 새 마스킹 폴리시(masking policy)를 만들거나 기존 마스킹 폴리시를 교체하는 명령이에요. 마스킹 폴리시는 테이블/뷰의 특정 컬럼에 적용해서 데이터를 가리는 방식이에요.

출처: 문서

본문

마스킹 폴리시를 만든 뒤에는 ALTER TABLE … ALTER COLUMN 명령으로 테이블의 컬럼에, 또는 ALTER VIEW 명령으로 뷰에 이 폴리시를 적용해요.

이 명령은 다음 변형(variant)을 지원해요:

  • CREATE MASKING POLICY: 새 마스킹 폴리시를 만듭니다.
  • CREATE OR ALTER MASKING POLICY: 없으면 만들고, 있으면 기존 폴리시를 수정합니다.

구문 (Syntax)

CREATE [ OR REPLACE ] MASKING POLICY [ IF NOT EXISTS ] <name> AS
( <arg_name_to_mask> <arg_type_to_mask> [ , <arg_1> <arg_type_1> ... ] )
RETURNS <arg_type_to_mask> -> <body>
[ COMMENT = '<string_literal>' ]
[ EXEMPT_OTHER_POLICIES = { TRUE | FALSE } ]

CREATE OR ALTER MASKING POLICY 구문

CREATE OR ALTER MASKING POLICY <name> AS
( <arg_name_to_mask> <arg_type_to_mask> [ , <arg_1> <arg_type_1> ... ] )
RETURNS <arg_type_to_mask> -> <body>
[ COMMENT = '<string_literal>' ]
[ EXEMPT_OTHER_POLICIES = { TRUE | FALSE } ]

필수 매개변수

  • <name>: 마스킹 폴리시의 식별자예요. 스키마 내에서 고유해야 해요. 기본 문자로 시작해야 하며, 전체 식별자를 큰따옴표로 감싸지 않는 한 공백이나 특수 문자를 포함할 수 없어요 (예: "My object"). 큰따옴표로 감싼 식별자는 대/소문자를 구분해요.
  • AS ( arg_name_to_mask arg_type_to_mask [ , arg_1 arg_type_1 ... ] ): 마스킹 폴리시의 시그니처(signature)로, 쿼리 실행 시 평가할 입력 컬럼과 데이터 타입을 지정해요.
    • arg_name_to_mask arg_type_to_mask: 첫 번째 컬럼과 그 데이터 타입으로, 이후 정책 조건에서 마스킹/토큰화할 컬럼 데이터 타입 값을 나타내요. 조건부 마스킹 폴리시에서 가상 컬럼(virtual column)을 첫 번째 컬럼 인자로 지정할 수는 없어요.
    • [ , arg_1 arg_type_1 ... ]: 쿼리 결과의 각 행에서 첫 번째 컬럼 데이터를 마스킹/토큰화할지 결정하는 조건부 컬럼과 그 데이터 타입이에요. 지정하지 않으면 Snowflake는 일반 마스킹 폴리시로 평가해요.
  • RETURNS arg_type_to_mask: 반환 데이터 타입은 입력 컬럼으로 지정된 첫 번째 컬럼의 입력 데이터 타입과 일치해야 해요.
  • <body>: arg_name_to_mask로 지정된 컬럼의 데이터를 변환하는 SQL 표현식이에요. 조건부 표현식 함수(Conditional expression functions), 내장 함수, 또는 UDF를 포함할 수 있어요.

선택 매개변수

  • COMMENT = 'string_literal': 마스킹 폴리시에 주석을 추가하거나 기존 주석을 덮어써요.

  • EXEMPT_OTHER_POLICIES = { TRUE | FALSE }: 사용 목적에 따라 다음 중 하나를 지정해요.

    • 행 접근 폴리시(row access policy)나 조건부 마스킹 폴리시가 이 마스킹 폴리시로 이미 보호된 컬럼을 참조할 수 있는지 여부.
    • 외부 테이블에서 가상 컬럼에 할당된 마스킹 폴리시가 VALUE 컬럼에서 상속한 마스킹 폴리시를 덮어쓸 수 있는지 여부.
    • TRUE: 다른 폴리시가 마스킹된 컬럼을 참조하거나, 가상 컬럼에 설정된 마스킹 폴리시가 VALUE 컬럼에서 상속한 것을 덮어쓰도록 허용.
    • FALSE: 허용하지 않음.

    이 속성의 값은 폴리시를 테이블/뷰에 설정한 뒤에는 변경할 수 없어요. 값을 갱신하려면 CREATE OR REPLACE MASKING POLICY 문을 실행해야 해요.

접근 제어 요구 사항

이 작업을 실행하려면 역할에 최소한 다음 권한이 필요해요:

권한 객체 참고
CREATE MASKING POLICY 스키마
OWNERSHIP 마스킹 폴리시 기존 마스킹 폴리시에 대해 CREATE OR ALTER MASKING POLICY 문 실행에 필요

EXEMPT_OTHER_POLICIES 속성을 지정할 때, 마스킹 폴리시를 소유한 역할(OWNERSHIP 권한이 있는 역할)은 행 접근 폴리시 또는 조건부 마스킹 폴리시를 소유한 역할의 역할 계층(role hierarchy)에 있어야 해요. 예:

masking_admin » rap_admin » SYSADMIN
masking_admin » cond_masking_admin » SYSADMIN

사용법 참고 사항

  • 기존 마스킹 폴리시를 교체하려면 현재 정의를 확인하기 위해 GET_DDL 함수를 호출하거나 DESCRIBE MASKING POLICY 명령을 실행하세요.
  • 마스킹 폴리시 본문에 서브쿼리를 포함할 때는 CASE 함수의 WHEN 분기에 EXISTS를 사용하세요.
  • 폴리시 body에 매핑 테이블 조회가 포함되면 보호된 테이블과 같은 데이터베이스에 중앙 집중식 매핑 테이블을 만드세요. 특히 IS_DATABASE_ROLE_IN_SESSION 함수를 호출할 때 중요해요.
  • 동일한 테이블/뷰 컬럼은 마스킹 폴리시 시그니처와 행 접근 폴리시 시그니처 중 하나에만 지정할 수 있어요.
  • 데이터 공유 제공자(provider)는 리더 계정(reader account)에 마스킹 폴리시를 만들 수 없어요.
  • 마스킹 폴리시 본문에 CURRENT_DATABASE 또는 CURRENT_SCHEMA 함수를 지정하면, 그 함수는 세션에서 사용 중인 데이터베이스/스키마가 아니라 보호된 테이블을 포함하는 데이터베이스/스키마를 반환해요.
  • OR REPLACEIF NOT EXISTS 절은 상호 배타적이에요. 같은 문에서 둘 다 사용할 수 없어요.
  • CREATE OR REPLACE <object> 문은 원자적이에요. 즉, 객체가 교체될 때 이전 객체가 삭제되고 새 객체가 단일 트랜잭션으로 생성돼요.

CREATE OR ALTER MASKING POLICY

  • ALTER MASKING POLICY 명령의 모든 제한이 적용돼요.
  • 기존 마스킹 폴리시의 시그니처(인자 이름과 데이터 타입)는 변경할 수 없어요. 시그니처를 바꿔야 한다면 기존 폴리시를 드롭하고 새로 만드세요.
  • 마스킹 폴리시 이름 변경은 지원되지 않아요.
  • 태그 설정/해제는 지원되지 않아요.

예제: 일반 마스킹 폴리시 (Normal masking policy)

마스킹 폴리시 본문을 작성하는 데 조건부 표현식 함수, 컨텍스트 함수, UDF를 사용할 수 있어요.

전체 마스크 (Full mask): analyst 사용자 지정 역할은 평문 값을 볼 수 있고, 그 외 사용자는 전체 마스크를 봐요.

CREATE OR REPLACE MASKING POLICY email_mask AS (val string) returns string ->
  CASE
    WHEN current_role() IN ('ANALYST') THEN VAL
    ELSE '*********'
  END;

프로덕션 계정만 마스킹 해제: 프로덕션 계정은 마스킹되지 않은 값을, 그 외 계정은 마스킹된 값을 보게 해요.

case
  when current_account() in ('<prod_account_identifier>') then val
  else '*********'
end;

미인가 사용자에게 NULL 반환:

case
  when current_role() IN ('ANALYST') then val
  else NULL
end;

미인가 사용자에게 고정 마스크 값 반환:

CASE
  WHEN current_role() IN ('ANALYST') THEN val
  ELSE '********'
END;

SHA2 / SHA2_HEX 사용한 해시 값 반환: 마스킹 폴리시에 해시 함수를 사용하면 충돌이 발생할 수 있으므로 주의하세요.

CASE
  WHEN current_role() IN ('ANALYST') THEN val
  ELSE sha2(val) -- return hash of the column value
END;

부분 마스크/전체 마스크 적용:

CASE
  WHEN current_role() IN ('ANALYST') THEN val
  WHEN current_role() IN ('SUPPORT') THEN regexp_replace(val,'.+\@','*****@') -- leave email domain unmasked
  ELSE '********'
END;

타임스탬프 사용:

case
  WHEN current_role() in ('SUPPORT') THEN val
  else date_from_parts(0001, 01, 01)::timestamp_ntz -- returns 0001-01-01 00:00:00.000
end;

중요: 현재 Snowflake는 마스킹 폴리시에서 서로 다른 입력/출력 데이터 타입을 지원하지 않아요 (예: 타임스탬프를 타게팅하고 문자열 ***MASKED*** 반환). 입력과 출력 데이터 타입은 일치해야 해요. 우회 방법으로 실제 타임스탬프 값을 위조된 타임스탬프 값으로 캐스팅할 수 있어요.

UDF 사용:

CASE
  WHEN current_role() IN ('ANALYST') THEN val
  ELSE mask_udf(val) -- custom masking function
END;

Variant 데이터에:

CASE
   WHEN current_role() IN ('ANALYST') THEN val
   ELSE OBJECT_INSERT(val, 'USER_IPADDRESS', '****', true)
END;

사용자 지정 자격 테이블(custom entitlement table) 사용: WHEN 절에 EXISTS를 사용하는 것에 주의하세요. 마스킹 폴리시 본문에 서브쿼리를 포함할 때는 항상 EXISTS를 사용해요.

CASE
  WHEN EXISTS
    (SELECT role FROM <db>.<schema>.entitlement WHERE mask_method='unmask' AND role = current_role()) THEN val
  ELSE '********'
END;

에이전트 활성 감지: IS_AGENT_ACTIVATED를 마스킹 폴리시에서 사용해 현재 실행 컨텍스트에 AI 에이전트가 활성화되어 있는지 식별하고 데이터 거버넌스 정책을 적용할 수 있어요.

CASE
  WHEN SYS_CONTEXT('SNOWFLAKE$CURRENT', 'IS_AGENT_ACTIVATED')::BOOLEAN = TRUE THEN '********'
  WHEN CURRENT_ROLE() IN ('ANALYST') THEN val
  ELSE '********'
END;

이전에 암호화된 데이터에 DECRYPT 사용 (ENCRYPT 또는 ENCRYPT_RAW, passphrase 사용):

case
  when current_role() in ('ANALYST') then DECRYPT(val, $passphrase)
  else val -- shows encrypted value
end;

JSON (VARIANT)에 JavaScript UDF 사용: 이 예에서 JavaScript UDF는 JSON 문자열의 위치 데이터를 마스킹해요. UDF와 마스킹 폴리시에서 데이터 타입을 VARIANT로 설정하는 것이 중요해요. 테이블 컬럼, UDF, 마스킹 폴리시 시그니처의 데이터 타입이 일치하지 않으면 Snowflake가 SQL을 해석할 수 없어 오류를 반환해요.

-- Flatten the JSON data

create or replace table <table_name> (v variant) as
select value::variant
from @<table_name>,
  table(flatten(input => parse_json($1):stationLocation));

-- JavaScript UDF to mask latitude, longitude, and location data

CREATE OR REPLACE FUNCTION full_location_masking(v variant)
  RETURNS variant
  LANGUAGE JAVASCRIPT
  AS
  $$
    if ("latitude" in V) {
      V["latitude"] = "**latitudeMask**";
    }
    if ("longitude" in V) {
      V["longitude"] = "**longitudeMask**";
    }
    if ("location" in V) {
      V["location"] = "**locationMask**";
    }

    return V;
  $$;

  -- Grant UDF usage to ACCOUNTADMIN

  grant ownership on function FULL_LOCATION_MASKING(variant) to role accountadmin;

  -- Create a masking policy using JavaScript UDF

  create or replace masking policy json_location_mask as (val variant) returns variant ->
    CASE
      WHEN current_role() IN ('ANALYST') THEN val
      else full_location_masking(val)
      -- else object_insert(val, 'latitude', '**locationMask**', true) -- limited to one value at a time
    END;

GEOGRAPHY 데이터 타입 사용: 이 예에서 마스킹 폴리시는 TO_GEOGRAPHY 함수를 사용해, CURRENT_ROLEANALYST가 아닌 사용자에 대해 컬럼의 모든 GEOGRAPHY 데이터를 캘리포니아 산 마테오(San Mateo)에 있는 Snowflake의 경위도 고정 지점으로 변환해요.

create masking policy mask_geo_point as (val geography) returns geography ->
  case
    when current_role() IN ('ANALYST') then val
    else to_geography('POINT(-122.35 37.55)')
  end;

GEOGRAPHY 데이터 타입 컬럼에 마스킹 폴리시를 설정하고 세션의 GEOGRAPHY_OUTPUT_FORMAT 값을 GeoJSON으로 설정해요:

alter table mydb.myschema.geography modify column b set masking policy mask_geo_point;
alter session set geography_output_format = 'GeoJSON';
use role public;
select * from mydb.myschema.geography;

Snowflake는 다음을 반환해요:

---+--------------------+
 A |         B          |
---+--------------------+
 1 | {                  |
   |   "coordinates": [ |
   |     -122.35,       |
   |     37.55          |
   |   ],               |
   |   "type": "Point"  |
   | }                  |
 2 | {                  |
   |   "coordinates": [ |
   |     -122.35,       |
   |     37.55          |
   |   ],               |
   |   "type": "Point"  |
   | }                  |
---+--------------------+

컬럼 B의 쿼리 결과 값은 세션의 GEOGRAPHY_OUTPUT_FORMAT 매개변수 값에 따라 달라져요. 예를 들어 값을 WKT로 설정하면:

alter session set geography_output_format = 'WKT';
select * from mydb.myschema.geography;

---+----------------------+
 A |         B            |
---+----------------------+
 1 | POINT(-122.35 37.55) |
 2 | POINT(-122.35 37.55) |
---+----------------------+

예제: 조건부 마스킹 폴리시 (Conditional masking policy)

다음 예는 CURRENT_ROLEadmin 사용자 지정 역할인 사용자, 또는 visibility 컬럼 값이 Public인 사용자에게 마스킹되지 않은 데이터를 반환해요. 그 외 조건은 모두 고정 마스크 값을 반환해요.

-- Conditional Masking

create masking policy email_visibility as
(email varchar, visibility string) returns varchar ->
  case
    when current_role() = 'ADMIN' then email
    when visibility = 'Public' then email
    else '***MASKED***'
  end;

다음 예는 CURRENT_ROLEadmin 사용자 지정 역할이고 다른 컬럼 값이 Public인 사용자에게 역토큰화(detokenized) 데이터를 반환해요. 그 외 조건은 토큰화된 값을 반환해요.

-- Conditional Tokenization

create masking policy de_email_visibility as
 (email varchar, visibility string) returns varchar ->
   case
     when current_role() = 'ADMIN' and visibility = 'Public' then de_email(email)
     else email -- sees tokenized data
   end;

예제: 행 접근 폴리시나 조건부 마스킹 폴리시에서 마스킹된 컬럼 허용

이메일 주소 보기, 이메일 주소 도메인만 보기, 또는 고정 마스크 값 중 하나를 허용하는 마스킹 폴리시를 교체해요:

create or replace masking policy governance.policies.email_mask
as (val string) returns string ->
case
  when current_role() in ('ANALYST') then val
  when current_role() in ('SUPPORT') then regexp_replace(val,'.+\@','*****@')
  else '********'
end
comment = 'specify in row access policy'
exempt_other_policies = true
;

이제 이 폴리시를 컬럼에 설정할 수 있고, 필요에 따라 행 접근 폴리시나 조건부 마스킹 폴리시가 이 마스킹 폴리시로 보호된 컬럼을 참조할 수 있어요.

CREATE OR ALTER MASKING POLICY

새 마스킹 폴리시를 만들거나 기존 폴리시의 본문을 교체해요:

CREATE OR ALTER MASKING POLICY email_mask AS (val string) RETURNS string ->
  CASE
    WHEN current_role() IN ('ANALYST') THEN val
    ELSE '*********'
  END
COMMENT = 'Mask email addresses from non-analyst roles';

더 알아보기 (Learn more)