# Proven Solution Patterns

Use this file when the user wants reusable patterns or examples of how to implement policies at scale.

## Pattern 1: Attribute-Based Access Control (ABAC)
Use user tags plus attribute mapping tables to drive masking or row filtering without duplicating policies.

**Why it's good**
- Centralizes authorization in a single mapping table; no policy duplication across tables.
- Scales with tags: add attributes or roles without rewriting policies.
- Keeps policy logic stable while access rules change in data.

**When to use**
- You have many columns/rows to protect and want one policy per data type or per tag.
- Access decisions depend on user attributes (role, department, clearance).
- You need to update access quickly without changing policy code.

**Context / tradeoffs**
- Requires governance discipline for tags and mapping table ownership.
- Per-row/tag lookups can add overhead; consider memoizable functions if needed.
- Best for consistent, organization-wide access rules rather than ad‑hoc exceptions.

### Column-Level Masking (Policy Uses Tags for Attributes)
```sql
-- Step 1: Setup
CREATE OR REPLACE DATABASE DEMO_DB;
USE DEMO_DB;

-- Step 2: Create access mapping table
CREATE OR REPLACE TABLE access_allowed (
	user_role_tag STRING,
	attribute STRING
);

-- HR can access HR_DATA; IT can access IT_DATA
INSERT INTO access_allowed VALUES
	('HR', 'HR_DATA'),
	('IT', 'IT_DATA');

-- Step 3: Create tags
CREATE OR REPLACE TAG user_attribute;
CREATE OR REPLACE TAG column_attribute;

-- Step 4: Create masking policy using column/user tags
CREATE OR REPLACE MASKING POLICY attribute_based_masking_policy
AS (val STRING)
RETURNS STRING ->
  CASE
	WHEN EXISTS (
    	SELECT 1
    	FROM access_allowed
    	WHERE attribute = SYSTEM$GET_TAG_ON_CURRENT_COLUMN('column_attribute')
      	AND user_role_tag = SYSTEM$GET_TAG('user_attribute', CURRENT_USER(), 'USER')
	)
	THEN val
	ELSE '***masked***'
  END;

-- Step 5: Attach masking policy to column tag
ALTER TAG column_attribute SET MASKING POLICY attribute_based_masking_policy;

-- Step 6: Create table with column tags
CREATE OR REPLACE TABLE employee_info (
	name STRING,
	ssn STRING WITH TAG (column_attribute = 'HR_DATA'),
	ip STRING WITH TAG (column_attribute = 'IT_DATA')
);

-- Step 7: Insert sample data
INSERT INTO employee_info VALUES
	('Alice', '123-45-6789', '192.168.0.1'),
	('Bob', '987-65-4321', '10.0.0.1');

-- Step 8a: Query as HR
ALTER USER ADMIN SET TAG user_attribute = 'HR';
-- ✅ HR sees SSN
-- ❌ HR sees IP as masked
SELECT '--- HR VIEW ---' AS view_context, * FROM employee_info;

-- Step 8b: Switch to IT role
ALTER USER ADMIN SET TAG user_attribute = 'IT';
-- ✅ IT sees IP
-- ❌ IT sees SSN as masked
SELECT '--- IT VIEW ---' AS view_context, * FROM employee_info;
```

### Row-Level Access (Policy Uses Tags for Attributes)
```sql
-- Step 1: Setup
CREATE OR REPLACE DATABASE DEMO_DB;
USE DEMO_DB;

-- Step 2: Create access mapping table
CREATE OR REPLACE TABLE access_allowed (
	user_role_tag STRING,
	row_attribute STRING
);

-- HR_MANAGER can access HR_DEPARTMENT; IT_MANAGER can access IT_DEPARTMENT
INSERT INTO access_allowed VALUES
	('HR_MANAGER', 'HR_DEPARTMENT'),
	('IT_MANAGER', 'IT_DEPARTMENT');

-- Step 3: Create user_attribute tag (only one tag needed)
CREATE OR REPLACE TAG user_attribute;

-- Step 4: Create row access policy using row_attribute column and user tag
CREATE OR REPLACE ROW ACCESS POLICY attribute_based_raw_access_policy
AS (row_attribute STRING)
RETURNS BOOLEAN ->
  EXISTS (
	SELECT 1
	FROM access_allowed
	WHERE row_attribute = access_allowed.row_attribute
  	AND user_role_tag = SYSTEM$GET_TAG('user_attribute', CURRENT_USER(), 'USER')
  );

-- Step 5: Create employee_info table with a row_attribute column
CREATE OR REPLACE TABLE employee_info (
	name STRING,
	ssn STRING,
	ip STRING,
	department STRING  -- This is the row_attribute: HR_DEPARTMENT, IT_DEPARTMENT
);

-- Step 6: Apply row access policy to department column
ALTER TABLE employee_info ADD ROW ACCESS POLICY attribute_based_raw_access_policy ON (department);

-- Step 7: Insert sample data
INSERT INTO employee_info VALUES
	('Alice', '123-45-6789', '192.168.0.1', 'HR_DEPARTMENT'),
	('Bob', '987-65-4321', '10.0.0.1', 'IT_DEPARTMENT');

-- Step 8a: Query as HR manager
ALTER USER ADMIN SET TAG user_attribute = 'HR_MANAGER';
-- ✅ HR_MANAGER sees only HR_DEPARTMENT rows
SELECT  '--- HR_MANAGER VIEW ---'AS view_context, * FROM employee_info;

-- Step 8b: Switch to IT manager
ALTER USER ADMIN SET TAG user_attribute = 'IT_MANAGER';
-- ✅ IT_MANAGER sees only IT_DEPARTMENT rows
SELECT '--- IT_MANAGER VIEW ---' AS view_context, * FROM employee_info;
```

## Pattern 2: Entitlement Table Pattern

**The core idea: Externalize access decisions to a data table.**

Instead of hardcoding role lists in policies, store access rules in an entitlement table. Policies query this table to make dynamic access decisions.

**Why it's good**
- Access changes are DATA changes, not CODE changes
- Adding/removing roles = INSERT/DELETE, no policy redeploy
- Auditability: entitlement table shows all access grants
- Supports complex access levels (FULL, MASKED, NONE)

**When to use**
- Many roles need different access levels
- Access rules change frequently
- You need audit trails of who can access what
- Multi-tenant or data-source-specific access control

### Entitlement Table Structure
```sql
-- Create policies schema (if it doesn't exist for this domain)
CREATE SCHEMA IF NOT EXISTS IT_SECURITY.POLICIES
COMMENT = 'Data governance policies for IT Security data';

-- Create entitlement table
CREATE OR REPLACE TABLE IT_SECURITY.POLICIES.DATA_ACCESS_CONTROL (
    ROLE_NAME VARCHAR,
    DATA_SOURCE VARCHAR,      -- 'ALL' or specific source
    ACCESS_LEVEL VARCHAR,     -- 'FULL', 'MASKED', or 'NONE'
    CONSTRAINT pk_access PRIMARY KEY (ROLE_NAME, DATA_SOURCE)
);

-- Populate with access rules
INSERT INTO IT_SECURITY.POLICIES.DATA_ACCESS_CONTROL VALUES
    ('IT_SECURITY_ADMIN_RL', 'ALL', 'FULL'),
    ('IT_SECURITY_ANALYST_RL', 'ALL', 'MASKED'),
    ('SECURITYADMIN', 'ALL', 'FULL'),
    ('DATA_SCIENTIST_RL', 'INTERNAL', 'MASKED'),
    ('DATA_SCIENTIST_RL', 'EXTERNAL', 'NONE');
```

### Masking Policy Using Entitlement Table
```sql
CREATE OR REPLACE MASKING POLICY IT_SECURITY.POLICIES.mask_pii_variant 
AS (val VARIANT, data_source VARCHAR) RETURNS VARIANT ->
  CASE
    -- Admin bypass
    WHEN IS_ROLE_IN_SESSION('IT_SECURITY_ADMIN_RL') THEN val
    -- Full access check
    WHEN EXISTS (
      SELECT 1 FROM IT_SECURITY.POLICIES.DATA_ACCESS_CONTROL
      WHERE IS_ROLE_IN_SESSION(ROLE_NAME)
      AND (DATA_SOURCE = data_source OR DATA_SOURCE = 'ALL')
      AND ACCESS_LEVEL = 'FULL'
    ) THEN val
    -- Masked access - remove sensitive fields
    WHEN EXISTS (
      SELECT 1 FROM IT_SECURITY.POLICIES.DATA_ACCESS_CONTROL
      WHERE IS_ROLE_IN_SESSION(ROLE_NAME)
      AND (DATA_SOURCE = data_source OR DATA_SOURCE = 'ALL')
      AND ACCESS_LEVEL = 'MASKED'
    ) THEN OBJECT_DELETE(OBJECT_DELETE(OBJECT_DELETE(val, 'email'), 'password'), 'ip_address')
    ELSE NULL  -- Secure default
  END
COMMENT = 'Masks PII in VARIANT data based on entitlement table. Owner: IT Security Team';
```

### Row Access Policy Using Entitlement Table
```sql
CREATE OR REPLACE ROW ACCESS POLICY IT_SECURITY.POLICIES.rap_data_access
AS (data_source VARCHAR) RETURNS BOOLEAN ->
  IS_ROLE_IN_SESSION('IT_SECURITY_ADMIN_RL')
  OR EXISTS (
    SELECT 1 FROM IT_SECURITY.POLICIES.DATA_ACCESS_CONTROL
    WHERE IS_ROLE_IN_SESSION(ROLE_NAME)
    AND (DATA_SOURCE = data_source OR DATA_SOURCE = 'ALL')
    AND ACCESS_LEVEL IN ('FULL', 'MASKED')
  )
COMMENT = 'Controls row-level access by data source. Owner: IT Security Team';
```

---

## Pattern 3: Split Pattern (Unmask Condition + Memoizable Function)

**The core idea: SPLIT unmask logic from masking logic.**

Instead of embedding role checks directly in each policy body, extract ("split") them into a single memoizable function. All policies then call this shared function.

```
❌ BEFORE (not split):              ✅ AFTER (split):
┌─────────────────────────┐        ┌─────────────────────────┐
│ MASK_STRING_PII         │        │ unmask_condition()      │  ← Single source of truth
│ ├─ CASE WHEN ROLE IN... │        │ └─ ROLE IN ('A','B')    │
│ └─ THEN val ELSE mask   │        └─────────────────────────┘
├─────────────────────────┤                    ▲
│ MASK_TIMESTAMP_PII      │                    │ calls
│ ├─ CASE WHEN ROLE IN... │  ──────────────────┼───────────────
│ └─ THEN val ELSE mask   │        ┌───────────┴───────────┐
└─────────────────────────┘        │                       │
 (duplicated logic)          ┌─────▼─────┐         ┌───────▼───────┐
                             │STRING_PII │         │TIMESTAMP_PII  │
                             │WHEN func()│         │WHEN func()    │
                             └───────────┘         └───────────────┘
```

**Why it's good**
- Single source of truth for "who can see cleartext"
- Adding a new role = update ONE function (all policies inherit the change)
- Faster evaluation when the same condition is used across many policies
- Cleaner policy definitions and easier audits

**When to use**
- Multiple masking policies share the same access rule
- You want consistent behavior across string/number/time/timestamp columns
- **Extending an existing policy to a new data type**
- You see hardcoded roles in an existing policy (refactor to split pattern)

**Context / tradeoffs**
- Function logic should be small and deterministic; avoid heavy queries.
- Memoization means changes to the condition require redeploying the function.
- Ensure return types match masking policy outputs (especially for time/date/timestamp).

### How to Recognize "Not Split" (Needs Refactoring)

Look at the existing policy body. If you see any of these, it's NOT split:

```sql
-- ❌ Hardcoded roles directly in policy
CASE WHEN CURRENT_ROLE() IN ('ROLE_A', 'ROLE_B') THEN val ELSE '***' END

-- ❌ Direct subquery in policy
CASE WHEN EXISTS (SELECT 1 FROM access_table WHERE ...) THEN val ELSE '***' END

-- ❌ Any unmask logic inline
CASE WHEN IS_ROLE_IN_SESSION('ADMIN') THEN val ELSE '***' END
```

If you see a function call, it IS split:

```sql
-- ✅ Calls a shared function (split pattern)
CASE WHEN GOVERNANCE.unmask_condition() THEN val ELSE '***' END
```

**Action:** When extending a policy that's NOT split, first apply the split pattern (extract logic to function), then create the new policy.

### Core Pattern: Memoizable Unmask Function
```sql
-- Step 1: Create the shared unmask condition function
CREATE OR REPLACE FUNCTION GOVERNANCE.unmask_condition()
RETURNS BOOLEAN
MEMOIZABLE
AS
$$
  CURRENT_ROLE() IN ('ACCOUNTADMIN', 'SECURITYADMIN', 'DATA_OWNER')
$$;
```

### Policy Templates by Data Type

Use these templates when extending protection to new data types:

```sql
-- STRING (VARCHAR) - for text PII like SSN, email, names
CREATE OR REPLACE MASKING POLICY GOVERNANCE.MASK_STRING_PII AS (val VARCHAR)
    RETURNS VARCHAR ->
    CASE
        WHEN GOVERNANCE.unmask_condition() THEN val
        ELSE '***MASKED***'
    END
COMMENT = 'Masks STRING PII. Authorized roles see cleartext.';

-- NUMBER - for numeric PII like salary, account numbers
CREATE OR REPLACE MASKING POLICY GOVERNANCE.MASK_NUMBER_PII AS (val NUMBER)
    RETURNS NUMBER ->
    CASE
        WHEN GOVERNANCE.unmask_condition() THEN val
        ELSE -1
    END
COMMENT = 'Masks NUMBER PII. Authorized roles see actual value.';

-- TIMESTAMP - for temporal PII like created_at, birth_datetime
CREATE OR REPLACE MASKING POLICY GOVERNANCE.MASK_TIMESTAMP_PII AS (val TIMESTAMP)
    RETURNS TIMESTAMP ->
    CASE
        WHEN GOVERNANCE.unmask_condition() THEN val
        ELSE '1900-01-01 00:00:00'::TIMESTAMP
    END
COMMENT = 'Masks TIMESTAMP PII. Authorized roles see actual timestamp.';

-- DATE - for date PII like birth_date, hire_date  
CREATE OR REPLACE MASKING POLICY GOVERNANCE.MASK_DATE_PII AS (val DATE)
    RETURNS DATE ->
    CASE
        WHEN GOVERNANCE.unmask_condition() THEN val
        ELSE '1900-01-01'::DATE
    END
COMMENT = 'Masks DATE PII. Authorized roles see actual date.';

-- TIME - for time PII like appointment_time
CREATE OR REPLACE MASKING POLICY GOVERNANCE.MASK_TIME_PII AS (val TIME)
    RETURNS TIME ->
    CASE
        WHEN GOVERNANCE.unmask_condition() THEN val
        ELSE '00:00:00'::TIME
    END
COMMENT = 'Masks TIME PII. Authorized roles see actual time.';
```

### Extending to a New Data Type

When adding protection for a new data type (e.g., customer has STRING policy, needs TIMESTAMP):

1. **Check if unmask function exists**: `SHOW USER FUNCTIONS IN SCHEMA GOVERNANCE;`
2. **If function doesn't exist**: Create it FIRST (see "Core Pattern" above)
3. **Create new policy using the function**: Copy template above, adjust return type and masked value
4. **Apply to columns**: `ALTER TABLE ... MODIFY COLUMN ... SET MASKING POLICY ...;`
5. **Verify consistency**: Test with same roles to confirm matching behavior

> ⚠️ **Order matters:** The function must exist BEFORE you create policies that reference it.
