Skip to content

Commit 31f2e3f

Browse files
fix(data-retention): enabled stage with no entity types redacts all (no fail-open)
1 parent a911af8 commit 31f2e3f

2 files changed

Lines changed: 17 additions & 9 deletions

File tree

apps/sim/lib/billing/retention.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ describe('resolveEffectivePiiRedaction', () => {
108108
})
109109
})
110110

111-
it('disables a stage that is enabled but has no entity types', () => {
111+
it('keeps an enabled stage active (redact all) when it has no entity types, and disables a disabled stage', () => {
112112
const result = resolveEffectivePiiRedaction({
113113
orgSettings: settings([
114114
{
@@ -123,7 +123,9 @@ describe('resolveEffectivePiiRedaction', () => {
123123
]),
124124
workspaceId: 'ws-1',
125125
})
126-
expect(result.input).toEqual(DISABLED)
126+
// enabled + empty entityTypes = redact ALL detected PII (not disabled).
127+
expect(result.input).toEqual({ enabled: true, entityTypes: [], language: 'en' })
128+
// disabled stage stays off regardless of its entity types.
127129
expect(result.blockOutputs).toEqual(DISABLED)
128130
expect(result.logs).toEqual({ enabled: true, entityTypes: ['PERSON'], language: 'en' })
129131
})

apps/sim/lib/billing/retention.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,18 @@ function sanitizeEntityTypes(value: unknown): string[] {
3737
return Array.isArray(value) ? value.filter((t): t is string => typeof t === 'string') : []
3838
}
3939

40-
/** A stage redacts nothing unless it is enabled AND has at least one entity type. */
40+
/**
41+
* Expand a stored stage policy into its effective form. A disabled stage redacts
42+
* nothing. An ENABLED stage with no entity types redacts ALL detected PII — the
43+
* masking layer omits `entities` from the Presidio request — so it stays active;
44+
* treating enabled-but-empty as disabled would let an explicit "redact all" save
45+
* silently skip masking (fail-open).
46+
*/
4147
function toEffectiveStage(policy: PiiStagePolicy | undefined): EffectivePiiStage {
42-
const types = sanitizeEntityTypes(policy?.entityTypes)
43-
if (!policy?.enabled || types.length === 0) return DISABLED_STAGE
48+
if (!policy?.enabled) return DISABLED_STAGE
4449
return {
4550
enabled: true,
46-
entityTypes: types,
51+
entityTypes: sanitizeEntityTypes(policy.entityTypes),
4752
language: coercePiiLanguage(policy.language) ?? DEFAULT_PII_LANGUAGE,
4853
}
4954
}
@@ -55,9 +60,10 @@ function toEffectiveStage(policy: PiiStagePolicy | undefined): EffectivePiiStage
5560
* selection is whole-rule; the selected rule is then expanded into three stages.
5661
*
5762
* Back-compat: a legacy rule with no `stages` is treated exactly as it was before
58-
* — logs-only, masking its flat `entityTypes` (input/blockOutputs disabled). A
59-
* resolved stage with no entity types redacts nothing. Defensive about the
60-
* loosely-typed JSON column.
63+
* — logs-only, masking its flat `entityTypes` (input/blockOutputs disabled); an
64+
* empty flat `entityTypes` redacts nothing (the workspace-exemption shape). For
65+
* per-stage rules an enabled stage with no entity types redacts ALL detected PII
66+
* (see {@link toEffectiveStage}). Defensive about the loosely-typed JSON column.
6167
*/
6268
export function resolveEffectivePiiRedaction(params: {
6369
orgSettings: DataRetentionSettings | null | undefined

0 commit comments

Comments
 (0)