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:
- 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.
- Enum sprawl — adding
STARTS_WITH, ENDS_WITH, and their NOT_ and CASE_INSENSITIVE_ variants would require 20+ enum values.
- 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/LIKE —
STARTS_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.
Problem
The current
SubjectMappingOperatorEnumconflates three concerns into a single enum:INNOT_ININ_CONTAINSThis creates several issues:
STARTS_WITHorENDS_WITH, so there's no safe way to match email domains.IN_CONTAINSwith@acme.comwould also matchuser@acme.com.badactor.ru.STARTS_WITH,ENDS_WITH, and theirNOT_andCASE_INSENSITIVE_variants would require 20+ enum values.Proposal
Decompose the condition into three orthogonal axes:
1. Comparison operator (how to compare two strings)
2. Quantifier (how to aggregate across value lists)
3. Case sensitivity modifier (bool flag)
Updated Condition message
Combination examples
INNOT_ININ_CONTAINS4 comparisons × 3 quantifiers × 2 case modes = 24 combinations from 3 enums + 1 bool, instead of 24 separate enum values.
Migration
Normalize the old
operatorfield early in the Go evaluation path:Old payloads continue to work. New payloads use the decomposed fields.
Scope
In scope
Conditionmessage and new enumssubject_mapping_builtin.goSubjectMappingOperatorEnumvaluesExplicitly out of scope (for now)
STARTS_WITH+ENDS_WITH+CONTAINScover the practical cases.Security motivation
The immediate driver is that
IN_CONTAINSis unsafe for domain matching —@acme.commatchesuser@acme.com.badactor.ru.ENDS_WITHis the correct operator for this use case, and the decomposed model makes it available without ad-hoc enum additions.