Skip to content

Decompose subject mapping operators into comparison, quantifier, and modifier axes #3335

@jrschumacher

Description

@jrschumacher

Problem

The current SubjectMappingOperatorEnum conflates three concerns into a single enum:

Current operator String comparison Quantifier Negation
IN exact match any-of no
NOT_IN exact match none-of yes
IN_CONTAINS substring any-of no

This creates several issues:

  1. Missing operators — there's no STARTS_WITH or ENDS_WITH, so there's no safe way to match email domains. IN_CONTAINS with @acme.com would also match user@acme.com.badactor.ru.
  2. Enum sprawl — adding STARTS_WITH, ENDS_WITH, and their NOT_ and CASE_INSENSITIVE_ variants would require 20+ enum values.
  3. No quantifier control — there's no way to express "the entity must have ALL of these values" without chaining multiple AND'd single-value conditions.

Proposal

Decompose the condition into three orthogonal axes:

1. Comparison operator (how to compare two strings)

enum ConditionComparisonOperator {
  CONDITION_COMPARISON_OPERATOR_EQUALS = 0;
  CONDITION_COMPARISON_OPERATOR_CONTAINS = 1;
  CONDITION_COMPARISON_OPERATOR_STARTS_WITH = 2;
  CONDITION_COMPARISON_OPERATOR_ENDS_WITH = 3;
}

2. Quantifier (how to aggregate across value lists)

enum ConditionQuantifier {
  CONDITION_QUANTIFIER_ANY = 0;   // at least one match (current IN behavior)
  CONDITION_QUANTIFIER_ALL = 1;   // every expected value is matched
  CONDITION_QUANTIFIER_NONE = 2;  // no matches (current NOT_IN behavior)
}

3. Case sensitivity modifier (bool flag)

bool case_insensitive = 7;

Updated Condition message

message Condition {
  string subject_external_selector_value = 1;
  repeated string subject_external_values = 2;

  // New decomposed fields
  ConditionComparisonOperator comparison = 5;
  ConditionQuantifier quantifier = 6;
  bool case_insensitive = 7;

  // Deprecated — old conflated operator, kept for backward compat
  SubjectMappingOperatorEnum operator = 3 [deprecated = true];
}

Combination examples

comparison quantifier case_insensitive Plain English Old equivalent
EQUALS ANY false value matches any in list IN
EQUALS NONE false value matches none in list NOT_IN
CONTAINS ANY false value contains any substring IN_CONTAINS
ENDS_WITH ANY true value ends with (case-insensitive) — new —
STARTS_WITH ALL false value starts with every prefix — new —
EQUALS ALL false entity has all of these values — new —

4 comparisons × 3 quantifiers × 2 case modes = 24 combinations from 3 enums + 1 bool, instead of 24 separate enum values.

Migration

Normalize the old operator field early in the Go evaluation path:

func normalizeCondition(c *Condition) {
    if c.Comparison != 0 || c.Quantifier != 0 {
        return // already using new fields
    }
    switch c.Operator {
    case IN:
        c.Comparison = EQUALS; c.Quantifier = ANY
    case NOT_IN:
        c.Comparison = EQUALS; c.Quantifier = NONE
    case IN_CONTAINS:
        c.Comparison = CONTAINS; c.Quantifier = ANY
    }
}

Old payloads continue to work. New payloads use the decomposed fields.

Scope

In scope

  • Proto changes to Condition message and new enums
  • Go evaluation logic in subject_mapping_builtin.go
  • Normalization/backward compatibility for old SubjectMappingOperatorEnum values
  • Tests for all new comparison × quantifier combinations
  • Documentation updates

Explicitly out of scope (for now)

  • REGEX comparison — performance risk in policy hot path, auditability concerns. Can be added as a comparison operator later without structural changes.
  • GLOB/LIKESTARTS_WITH + ENDS_WITH + CONTAINS cover the practical cases.
  • ALL_EXTRACTED quantifier ("every value the user has must be in this allowed list") — rare use case, can add as a fourth quantifier later if needed.

Security motivation

The immediate driver is that IN_CONTAINS is unsafe for domain matching — @acme.com matches user@acme.com.badactor.ru. ENDS_WITH is the correct operator for this use case, and the decomposed model makes it available without ad-hoc enum additions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions