|
87 | 87 |
|
88 | 88 | > **See also:** DS-14 (schema-level deny), DS-15 (table-level deny) in `permission_stories.md` |
89 | 89 |
|
| 90 | +### Validate `deny` + `column_mask` Combination |
| 91 | + |
| 92 | +- **Problem**: The codebase silently ignores `column_mask` obligations on deny-effect policies. The `PolicyHook` only processes `column_mask` from permit policies, so if a user creates a deny policy with a `column_mask` obligation, it has no effect — the column is not masked. |
| 93 | +- **Recommendation**: Add validation in both API and UI to prevent this invalid combination: |
| 94 | + - **API**: Reject policy creation/update if `effect: "deny"` and `obligation_type: "column_mask"` are both present. Return a clear validation error. |
| 95 | + - **UI**: When "deny" effect is selected, hide `column_mask` from the available obligation types in the policy creation form. Show a tooltip or help text explaining why (e.g., "Column masking is not supported on deny policies"). |
| 96 | + |
| 97 | +### Conditional Column Masking |
| 98 | + |
| 99 | +- **Use case**: Mask sensitive columns only when certain user attributes match a condition. For example: |
| 100 | + - Mask `salary` when `user.team != 'finance'` |
| 101 | + - Mask `ssn` when `user.role != 'admin'` |
| 102 | + - Mask `customer_email` when `user.region != user.customer_region` |
| 103 | +- **Proposed syntax**: |
| 104 | + ```json |
| 105 | + { |
| 106 | + "schema": "hr", |
| 107 | + "table": "employees", |
| 108 | + "column": "salary", |
| 109 | + "mask_expression": "'***'", |
| 110 | + "condition": "user.team != 'finance'" |
| 111 | + } |
| 112 | + ``` |
| 113 | +- **Behavior**: If the condition evaluates to true, apply the mask. If false, return the original column value. |
| 114 | +- **Implementation**: Extend `ColumnMaskDef` with an optional `condition` field. At query time, evaluate the condition (similar to how `{user.*}` variables are substituted) and only apply the mask expression if true. |
| 115 | +- **Alternative**: Could also support "mask else original" semantics where a different mask is applied when condition is false, but the simple conditional masking covers most use cases. |
| 116 | + |
| 117 | +### Conditional Obligations (All Types) |
| 118 | + |
| 119 | +Should every obligation type support conditional application? This would allow policies that activate only under certain conditions: |
| 120 | + |
| 121 | +| Obligation Type | Conditional Use Case | Complexity | |
| 122 | +|-----------------|---------------------|------------| |
| 123 | +| `row_filter` | Filter rows only for non-admin users | Low - filter_expression IS the condition | |
| 124 | +| `column_mask` | Mask sensitive data for non-permissioned users | Medium - proposed above | |
| 125 | +| `column_access deny` | Hide columns from non-admin users | Medium | |
| 126 | +| `object_access deny` | Hide schemas/tables from certain teams | Medium | |
| 127 | + |
| 128 | +**Option A: Per-obligation condition field (recommended)** |
| 129 | + |
| 130 | +Each obligation type gets an optional `condition` field: |
| 131 | + |
| 132 | +```json |
| 133 | +// Row filter - condition is redundant, filter_expression IS the condition |
| 134 | +{ |
| 135 | + "obligation_type": "row_filter", |
| 136 | + "condition": "user.role != 'admin'", // redundant, but consistent |
| 137 | + "definition": { "schema": "orders", "table": "*", "filter_expression": "tenant_id = {user.tenant}" } |
| 138 | +} |
| 139 | + |
| 140 | +// Column mask - condition adds fine-grained control |
| 141 | +{ |
| 142 | + "obligation_type": "column_mask", |
| 143 | + "condition": "user.role != 'admin'", |
| 144 | + "definition": { "schema": "hr", "table": "employees", "column": "ssn", "mask_expression": "'***-**-****'" } |
| 145 | +} |
| 146 | + |
| 147 | +// Column access deny - hide columns conditionally |
| 148 | +{ |
| 149 | + "obligation_type": "column_access", |
| 150 | + "condition": "user.clearance_level < 5", |
| 151 | + "definition": { "schema": "secret", "table": "files", "action": "deny", "columns": ["content"] } |
| 152 | +} |
| 153 | + |
| 154 | +// Object access deny - hide tables conditionally |
| 155 | +{ |
| 156 | + "obligation_type": "object_access", |
| 157 | + "condition": "user.team != 'executive'", |
| 158 | + "definition": { "schema": "analytics", "action": "deny" } |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 162 | +**Option B: Condition IN the obligation definition** |
| 163 | + |
| 164 | +Embed the condition inside the definition object rather than as a top-level field. More compact but less consistent across types. |
| 165 | + |
| 166 | +**Option C: Split into two policies** |
| 167 | + |
| 168 | +Current workaround: Create separate policies for each condition. For example: |
| 169 | +- Policy 1: `row_filter` for regular users |
| 170 | +- Policy 2: No obligations for admins (implicit allow) |
| 171 | + |
| 172 | +This works but creates policy explosion when combining multiple conditions. |
| 173 | + |
| 174 | +**Recommendation**: Go with **Option A** - add optional `condition` field to all obligation types. This provides: |
| 175 | +- Consistency across all obligation types |
| 176 | +- Clear semantics: obligation only applies when condition is true |
| 177 | +- Future-proof: easy to extend with more complex condition expressions |
| 178 | +- Backward compatible: condition is optional, existing policies work unchanged |
| 179 | + |
| 180 | +**Condition expression syntax**: |
| 181 | +- Reuse same expression parser as `filter_expression` / `mask_expression` |
| 182 | +- Available variables: `{user.*}` substitutions (tenant, username, id, role, team, etc.) |
| 183 | +- Operators: `=`, `!=`, `<`, `>`, `<=`, `>=`, `AND`, `OR`, `NOT`, `IN` |
| 184 | +- Examples: `user.role = 'admin'`, `user.team NOT IN ('sales', 'marketing')`, `user.clearance_level >= 3` |
| 185 | + |
| 186 | +**Priority when multiple conditions match**: |
| 187 | +- If multiple policies have conditions that all evaluate to true, apply all obligations (AND semantics, same as now) |
| 188 | +- If a condition evaluates to false, that obligation is skipped |
| 189 | +- Order: evaluate all conditions first, then apply matching obligations |
| 190 | + |
| 191 | +**Implementation plan**: |
| 192 | +1. Add `condition` field to `Obligation` struct (optional, nullable) |
| 193 | +2. Add condition evaluation helper (reuses existing `{user.*}` substitution logic) |
| 194 | +3. Update `ObligationEffects::collect()` to check condition before including each obligation |
| 195 | +4. Update tests to cover conditional obligations |
| 196 | + |
| 197 | +> **See also:** Related to DM-03 (mask vs. hide decision), DM-04 (canary rollout for testing policies on subset of users) |
| 198 | +
|
90 | 199 | ## UI/UX Improvements |
91 | 200 |
|
92 | 201 | ### User Name, Datasource Name, Policy Name Validation |
@@ -240,6 +349,13 @@ Given complexity of new policy system (interaction with DataFusion and PostgreSQ |
240 | 349 | - 2026-03-08: Row filter policy interaction bug - when two separate row filter policies are enabled (e.g., tenant filter on tenant='foo' AND state filter on state!='WY'), the result contains more rows than either policy alone. Both tenant 'foo' rows AND non-WY state rows appear, rather than rows satisfying BOTH conditions. |
241 | 350 | - Sometimes SQL queries take long time and cause UI to hang - need performance testing, may be missing indexes |
242 | 351 |
|
| 352 | +### Git Commit Hook Improvements |
| 353 | + |
| 354 | +- Current: pre-commit hook runs `cargo fmt`, `cargo clippy`, `npm run typecheck`, `npm run test:run` on ALL changes (staged + unstaged) |
| 355 | +- Problem: Uncommitted unstaged changes cause false failures - hook fails because of code in files you haven't committed yet |
| 356 | +- Proposed: Modify hook to only check staged changes using `git diff --staged` or `git diff --cached` |
| 357 | +- This preserves ability to have WIP changes without them interfering with the commit process |
| 358 | + |
243 | 359 | ## Frontend Architecture Guideline: Future-Proofing UI |
244 | 360 |
|
245 | 361 | ### 1. Decouple Logic from Presentation |
|
0 commit comments