diff --git a/src/main/java/com/evoila/janus/common/enforcement/core/OperatorHandlers.java b/src/main/java/com/evoila/janus/common/enforcement/core/OperatorHandlers.java
deleted file mode 100644
index 2276d62..0000000
--- a/src/main/java/com/evoila/janus/common/enforcement/core/OperatorHandlers.java
+++ /dev/null
@@ -1,324 +0,0 @@
-package com.evoila.janus.common.enforcement.core;
-
-import com.evoila.janus.common.enforcement.model.dto.EnhancementData;
-import com.evoila.janus.common.enforcement.utils.LabelPatternUtils;
-import java.util.Optional;
-import java.util.Set;
-import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
-import java.util.stream.Collectors;
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * Handles the specific logic for different label operators (=, !=, =~, !~).
- *
- *
This class provides operator-specific logic for label enhancement: - Equals operator (=):
- * Exact value matching with security validation - Not equals operator (!=): Inverse matching with
- * constraint conversion - Regex match operator (=~): Pattern matching with allowed value filtering
- * - Regex not match operator (!~): Inverse pattern matching with constraint conversion
- *
- *
Each operator handler validates input against security constraints and converts operators to
- * appropriate constraint expressions when needed.
- */
-@Slf4j
-public final class OperatorHandlers {
-
- // Expression building constants
- private static final String QUOTE_CHAR = "\"";
-
- // Error message constants
- private static final String UNAUTHORIZED_LABEL_VALUE_EXCEPTION_MSG = "Unauthorized label value: ";
-
- private OperatorHandlers() {
- // Utility class - prevent instantiation
- }
-
- /**
- * Builds a constraint expression from a set of allowed values.
- *
- *
This method creates appropriate constraint expressions: - Single value: Uses exact match
- * (e.g., "namespace=\"demo\"") - Multiple values: Uses regex match (e.g.,
- * "namespace=~\"demo|observability\"")
- *
- * @param labelName The label name to build constraint for
- * @param values The set of allowed values for the label
- * @return Constraint expression string with appropriate operator
- */
- private static String buildConstraintFromValues(String labelName, Set values) {
- if (values.size() == 1) {
- // Single value - use exact match
- String value = values.iterator().next();
- return labelName + "=\"" + value + "\"";
- } else {
- // Multiple values - use regex match
- String pattern = String.join("|", values);
- return labelName + "=~\"" + pattern + "\"";
- }
- }
-
- /**
- * Checks if a value matches a regex pattern with robust error handling.
- *
- * This method safely tests regex patterns and handles invalid patterns gracefully: - Compiles
- * the regex pattern with error handling - Returns true if the value matches the pattern - Falls
- * back to substring matching for invalid regex patterns
- *
- * @param regexPattern The regex pattern to compile and test
- * @param valueToTest The value to test against the pattern
- * @return true if the value matches the pattern, false otherwise
- */
- private static boolean matchesRegexPattern(String regexPattern, String valueToTest) {
- try {
- Pattern pattern = Pattern.compile(regexPattern);
- return pattern.matcher(valueToTest).matches();
- } catch (PatternSyntaxException _) {
- // If regex is invalid, treat it as a literal string match
- return regexPattern.contains(valueToTest) || valueToTest.contains(regexPattern);
- }
- }
-
- /**
- * Handles the equals (=) operator for label enhancement.
- *
- *
This method processes equals operators by: - Validating the value against allowed
- * constraints - Handling wildcard values appropriately - Preserving exact matches when
- * constraints allow - Throwing SecurityException for unauthorized values
- *
- * @param data EnhancementData containing label information and constraints
- * @return Optional containing the enhanced label expression, or empty if no enhancement
- * @throws SecurityException if the value is not allowed by constraints
- */
- public static Optional handleEquals(EnhancementData data) {
- if (data.isValueWildcard()) {
- return handleWildcardValue(data);
- }
-
- validateEqualsValue(data);
-
- return Optional.of(data.originalOrReconstructed());
- }
-
- private static void validateEqualsValue(EnhancementData data) {
- if (data.allowedValues() == null || data.allowedValues().isEmpty()) {
- log.debug(
- "OperatorHandlers: No constraints defined for label '{}', allowing value '{}'",
- data.labelName(),
- data.value());
- return;
- }
-
- if (hasWildcardConstraint(data.allowedValues())) {
- log.debug(
- "OperatorHandlers: Wildcard constraints found for label '{}', allowing value '{}'",
- data.labelName(),
- data.value());
- return;
- }
-
- if (data.allowedValues().contains(data.value())) {
- return;
- }
-
- boolean matchesPattern =
- data.allowedValues().stream()
- .filter(LabelPatternUtils::isFullRegexPattern)
- .anyMatch(pattern -> matchesRegexPattern(pattern, data.value()));
-
- if (matchesPattern) {
- log.debug(
- "OperatorHandlers: Value '{}' matches regex pattern in allowed values for label '{}'",
- data.value(),
- data.labelName());
- return;
- }
-
- throw new SecurityException(UNAUTHORIZED_LABEL_VALUE_EXCEPTION_MSG + data.value());
- }
-
- private static boolean hasWildcardConstraint(Set allowedValues) {
- return allowedValues.stream()
- .anyMatch(
- v ->
- LabelPatternUtils.isWildcardPattern(v)
- || v.contains(LabelPatternUtils.WILDCARD_ASTERISK));
- }
-
- /**
- * Handles the not equals (!=) operator for label enhancement.
- *
- * This method processes not-equals operators by: - Converting to remaining allowed values when
- * constraints exist - Preserving original not-equals when no constraints are defined - Handling
- * empty string values specially for != operators - Throwing SecurityException when no valid
- * values remain
- *
- * @param data EnhancementData containing label information and constraints
- * @return Optional containing the enhanced label expression, or empty if no enhancement
- * @throws SecurityException if no valid values remain after filtering
- */
- public static Optional handleNotEquals(EnhancementData data) {
- // Special case: for != operator with empty string, preserve it as-is
- // Empty strings should not be treated as wildcards for != operators
- if (data.value() != null
- && data.value().isEmpty()
- && LabelPatternUtils.NOT_EQUALS_OPERATOR.equals(data.operator())) {
- return Optional.of(data.originalOrReconstructed());
- }
-
- if (data.isValueWildcard()) {
- return handleWildcardValue(data);
- }
-
- // For not-equals operator, we need to convert it to the remaining allowed values
- if (data.hasSpecificConstraints()) {
- Set remainingValues =
- data.allowedValues().stream()
- .filter(v -> !v.equals(data.value()))
- .collect(Collectors.toSet());
-
- if (remainingValues.isEmpty()) {
- // No remaining values - this should not happen as it would be caught by validation
- throw new SecurityException(UNAUTHORIZED_LABEL_VALUE_EXCEPTION_MSG + data.value());
- } else {
- return Optional.of(buildConstraintFromValues(data.labelName(), remainingValues));
- }
- } else {
- // No specific constraints, preserve original not-equals
- return Optional.of(data.originalOrReconstructed());
- }
- }
-
- /**
- * Handles the regex match (=~) operator for label enhancement.
- *
- * This method processes regex match operators by: - Testing the regex pattern against allowed
- * values - Converting to matching allowed values when constraints exist - Preserving original
- * regex match when no constraints are defined - Throwing SecurityException when no values match
- * the pattern
- *
- * @param data EnhancementData containing label information and constraints
- * @return Optional containing the enhanced label expression, or empty if no enhancement
- * @throws SecurityException if no allowed values match the regex pattern
- */
- public static Optional handleRegexMatch(EnhancementData data) {
- if (data.isValueWildcard()) {
- return handleWildcardValue(data);
- }
-
- // For regex operators, we need to check if the pattern matches any allowed values
- if (data.hasSpecificConstraints()) {
- // Find all allowed values that match the regex pattern
- Set matchingValues =
- data.allowedValues().stream()
- .filter(allowedValue -> matchesRegexPattern(data.value(), allowedValue))
- .collect(Collectors.toSet());
-
- if (matchingValues.isEmpty()) {
- throw new SecurityException(UNAUTHORIZED_LABEL_VALUE_EXCEPTION_MSG + data.value());
- } else {
- return Optional.of(buildConstraintFromValues(data.labelName(), matchingValues));
- }
- }
-
- // Reconstruct explicitly — originalText may have a different operator (= converted to =~)
- String q = data.quoted() ? QUOTE_CHAR : "";
- return Optional.of(
- data.labelName() + LabelPatternUtils.REGEX_MATCH_OPERATOR + q + data.value() + q);
- }
-
- /**
- * Handles the regex not match (!~) operator for label enhancement.
- *
- * This method processes regex not-match operators by: - Filtering out values that match the
- * regex pattern - Converting to remaining allowed values when constraints exist - Preserving
- * original regex not-match when no constraints are defined - Throwing SecurityException when all
- * values are excluded by the pattern
- *
- * @param data EnhancementData containing label information and constraints
- * @return Optional containing the enhanced label expression, or empty if no enhancement
- * @throws SecurityException if all allowed values are excluded by the regex pattern
- */
- public static Optional handleRegexNotMatch(EnhancementData data) {
- // For !~ operator, we should always process the value as a regex pattern, even if it's a
- // wildcard
-
- // Handle null allowed values - no constraints defined for this label
- if (data.allowedValues() == null) {
- log.debug(
- "OperatorHandlers: No constraints defined for label '{}', preserving original !~ operator",
- data.labelName());
- // Reconstruct explicitly — originalText may have != (converted to !~)
- String q = data.quoted() ? QUOTE_CHAR : "";
- return Optional.of(
- data.labelName() + LabelPatternUtils.REGEX_NOT_MATCH_OPERATOR + q + data.value() + q);
- }
-
- // Handle empty allowed values - return empty to indicate no enhancement possible
- if (data.allowedValues().isEmpty()) {
- log.debug(
- "OperatorHandlers: Empty allowed values for label '{}', returning empty",
- data.labelName());
- return Optional.empty();
- }
-
- // For regex not match (!~), we need to convert it to a regex match with remaining allowed
- // values
- if (data.hasSpecificConstraints()) {
- log.info(
- "OperatorHandlers: Processing !~ operator with pattern '{}' and allowed values: {}",
- data.value(),
- data.allowedValues());
-
- // Filter out values that match the regex pattern
- Set remainingValues =
- data.allowedValues().stream()
- .filter(
- allowedValue -> {
- boolean matches = matchesRegexPattern(data.value(), allowedValue);
- log.info(
- "OperatorHandlers: Checking if '{}' matches pattern '{}': {}",
- allowedValue,
- data.value(),
- matches);
- return !matches;
- })
- .collect(Collectors.toSet());
-
- log.info("OperatorHandlers: Remaining values after filtering: {}", remainingValues);
-
- if (remainingValues.isEmpty()) {
- // All values are excluded - this should not be allowed
- log.error(
- "OperatorHandlers: All values excluded by pattern '{}', throwing SecurityException",
- data.value());
- throw new SecurityException(UNAUTHORIZED_LABEL_VALUE_EXCEPTION_MSG + data.value());
- } else {
- return Optional.of(buildConstraintFromValues(data.labelName(), remainingValues));
- }
- } else {
- // No specific constraints, preserve original not-regex-match
- // Reconstruct explicitly — originalText may have != (converted to !~)
- String q = data.quoted() ? QUOTE_CHAR : "";
- return Optional.of(
- data.labelName() + LabelPatternUtils.REGEX_NOT_MATCH_OPERATOR + q + data.value() + q);
- }
- }
-
- /**
- * Handles wildcard values for any operator type.
- *
- * This method processes wildcard values by: - Expanding wildcards to all allowed values when
- * constraints exist - Returning empty when no constraints are defined - Building constraint
- * expressions from allowed value sets
- *
- * @param data EnhancementData containing label information and constraints
- * @return Optional containing the enhanced label expression, or empty if no constraints exist
- */
- private static Optional handleWildcardValue(EnhancementData data) {
- if (data.allowedValues() == null || data.allowedValues().isEmpty()) {
- return Optional.empty();
- }
-
- // If we have specific constraints, expand wildcard to allowed values
- return Optional.of(buildConstraintFromValues(data.labelName(), data.allowedValues()));
- }
-}
diff --git a/src/main/java/com/evoila/janus/common/enforcement/label/LabelEnhancer.java b/src/main/java/com/evoila/janus/common/enforcement/label/LabelEnhancer.java
new file mode 100644
index 0000000..c77dbee
--- /dev/null
+++ b/src/main/java/com/evoila/janus/common/enforcement/label/LabelEnhancer.java
@@ -0,0 +1,429 @@
+package com.evoila.janus.common.enforcement.label;
+
+import com.evoila.janus.common.enforcement.model.dto.LabelExpression;
+import com.evoila.janus.common.enforcement.utils.LabelPatternUtils;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import java.util.stream.Collectors;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Applies security constraints to normalized {@link LabelExpression} objects.
+ *
+ * This class handles:
+ *
+ *
+ * - Operator-specific enforcement (=, !=, =~, !~)
+ *
- Wildcard expansion to allowed values
+ *
- Adding missing constraint labels
+ *
- Validation of enhanced expressions
+ *
+ */
+@Slf4j
+public final class LabelEnhancer {
+
+ private static final String UNAUTHORIZED_MSG = "Unauthorized label value: ";
+
+ /** Operator prefixes that can be embedded in constraint values (check order: longest first). */
+ private static final String[] VALUE_OPERATOR_PREFIXES = {
+ LabelPatternUtils.REGEX_NOT_MATCH_OPERATOR, // !~
+ LabelPatternUtils.REGEX_MATCH_OPERATOR, // =~
+ LabelPatternUtils.NOT_EQUALS_OPERATOR // !=
+ };
+
+ private LabelEnhancer() {}
+
+ /**
+ * Applies security constraints to a list of label expressions.
+ *
+ * @param expressions The normalized expressions to enhance
+ * @param constraints The security constraints (label name → allowed values)
+ * @return New list with enhanced expressions; expressions that cannot be enhanced are removed
+ */
+ public static List enhance(
+ List expressions, Map> constraints) {
+ List result = new ArrayList<>();
+
+ for (LabelExpression expr : expressions) {
+ // Passthrough expressions are preserved as-is
+ if (expr.passthrough()) {
+ result.add(expr);
+ continue;
+ }
+
+ Set allowedValues = constraints.get(expr.name());
+
+ // No enforcement constraints and no special handling needed: preserve original
+ if (allowedValues == null
+ && !LabelPatternUtils.isEmptyOrWildcard(expr.value())
+ && !LabelPatternUtils.isRegexPattern(expr.value())) {
+ result.add(expr);
+ continue;
+ }
+
+ log.debug(
+ "LabelEnhancer: Processing '{}' op='{}' value='{}' allowed={}",
+ expr.name(),
+ expr.operator(),
+ expr.value(),
+ allowedValues);
+
+ Optional enhanced = enhanceSingle(expr, allowedValues);
+ enhanced.ifPresent(result::add);
+ }
+
+ return result;
+ }
+
+ /**
+ * Adds constraint labels that are missing from the current expression list.
+ *
+ * @param expressions The current list of expressions
+ * @param constraints The full set of security constraints
+ * @return New list with missing constraints appended
+ */
+ public static List addMissingConstraints(
+ List expressions, Map> constraints) {
+ Set existingNames =
+ expressions.stream()
+ .filter(e -> !e.passthrough())
+ .map(LabelExpression::name)
+ .collect(Collectors.toSet());
+
+ List result = new ArrayList<>(expressions);
+
+ for (Map.Entry> entry : constraints.entrySet()) {
+ String labelName = entry.getKey();
+ Set allowedValues = entry.getValue();
+
+ // Skip configuration keys and already existing labels
+ if (LabelPatternUtils.CONFIGURATION_KEYS.contains(labelName)
+ || "labels".equals(labelName)
+ || existingNames.contains(labelName)) {
+ continue;
+ }
+
+ // Skip wildcard constraints
+ if (allowedValues == null
+ || allowedValues.isEmpty()
+ || LabelPatternUtils.containsWildcardValues(allowedValues)) {
+ continue;
+ }
+
+ // Build constraint expression for this label (always =~ for missing constraints)
+ LabelExpression constraint = buildMissingConstraintExpression(labelName, allowedValues);
+ result.add(constraint);
+ log.debug("LabelEnhancer: Added missing constraint: {}", constraint.serialize());
+ }
+
+ return result;
+ }
+
+ /**
+ * Validates enhanced expressions against constraints. Filters out expressions with values that
+ * are not allowed.
+ *
+ * @param expressions The enhanced expressions to validate
+ * @param constraints The security constraints
+ * @return New list with only valid expressions
+ */
+ public static List validate(
+ List expressions, Map> constraints) {
+ return expressions.stream().filter(expr -> isValid(expr, constraints)).toList();
+ }
+
+ // ---------------------------------------------------------------------------
+ // Single expression enhancement (replaces OperatorHandlers + STRATEGY_FACTORIES)
+ // ---------------------------------------------------------------------------
+
+ private static Optional enhanceSingle(
+ LabelExpression expr, Set allowedValues) {
+ boolean isValueWildcard = LabelPatternUtils.isEmptyOrWildcard(expr.value());
+ boolean hasWildcardConstraints = allowedValues != null && hasAnyWildcard(allowedValues);
+ boolean hasSpecificConstraints =
+ allowedValues != null && !allowedValues.isEmpty() && !hasWildcardConstraints;
+
+ // Handle wildcard values for =~ operator (but NOT for !~)
+ if (!LabelPatternUtils.REGEX_NOT_MATCH_OPERATOR.equals(expr.operator())
+ && LabelPatternUtils.isWildcardPatternForEnhancement(expr.value(), expr.operator())) {
+ return handleWildcardPattern(expr, allowedValues);
+ }
+
+ return switch (expr.operator()) {
+ case "=" ->
+ handleEquals(
+ expr, allowedValues, isValueWildcard, hasWildcardConstraints, hasSpecificConstraints);
+ case "!=" -> handleNotEquals(expr, allowedValues, isValueWildcard, hasSpecificConstraints);
+ case "=~" -> handleRegexMatch(expr, allowedValues, isValueWildcard, hasSpecificConstraints);
+ case "!~" -> handleRegexNotMatch(expr, allowedValues, hasSpecificConstraints);
+ default -> {
+ log.warn("LabelEnhancer: Unknown operator: {}", expr.operator());
+ yield Optional.empty();
+ }
+ };
+ }
+
+ // --- Equals (=) ---
+
+ private static Optional handleEquals(
+ LabelExpression expr,
+ Set allowedValues,
+ boolean isValueWildcard,
+ boolean hasWildcardConstraints,
+ boolean hasSpecificConstraints) {
+ if (isValueWildcard) {
+ return expandWildcard(expr, allowedValues);
+ }
+ validateEqualsValue(expr, allowedValues, hasWildcardConstraints);
+ return Optional.of(expr);
+ }
+
+ private static void validateEqualsValue(
+ LabelExpression expr, Set allowedValues, boolean hasWildcardConstraints) {
+ if (allowedValues == null || allowedValues.isEmpty()) {
+ return;
+ }
+ if (hasWildcardConstraints) {
+ return;
+ }
+ if (allowedValues.contains(expr.value())) {
+ return;
+ }
+ boolean matchesPattern =
+ allowedValues.stream()
+ .filter(LabelPatternUtils::isFullRegexPattern)
+ .anyMatch(pattern -> matchesRegex(pattern, expr.value()));
+ if (matchesPattern) {
+ return;
+ }
+ throw new SecurityException(UNAUTHORIZED_MSG + expr.value());
+ }
+
+ // --- Not Equals (!=) ---
+
+ private static Optional handleNotEquals(
+ LabelExpression expr,
+ Set allowedValues,
+ boolean isValueWildcard,
+ boolean hasSpecificConstraints) {
+ // Preserve != with empty string as-is
+ if (expr.value() != null && expr.value().isEmpty()) {
+ return Optional.of(expr);
+ }
+ if (isValueWildcard) {
+ return expandWildcard(expr, allowedValues);
+ }
+ if (hasSpecificConstraints) {
+ Set remaining =
+ allowedValues.stream().filter(v -> !v.equals(expr.value())).collect(Collectors.toSet());
+ if (remaining.isEmpty()) {
+ throw new SecurityException(UNAUTHORIZED_MSG + expr.value());
+ }
+ return Optional.of(buildConstraintExpression(expr.name(), remaining));
+ }
+ return Optional.of(expr);
+ }
+
+ // --- Regex Match (=~) ---
+
+ private static Optional handleRegexMatch(
+ LabelExpression expr,
+ Set allowedValues,
+ boolean isValueWildcard,
+ boolean hasSpecificConstraints) {
+ if (isValueWildcard) {
+ return expandWildcard(expr, allowedValues);
+ }
+ if (hasSpecificConstraints) {
+ Set matching =
+ allowedValues.stream()
+ .filter(v -> matchesRegex(expr.value(), v))
+ .collect(Collectors.toSet());
+ if (matching.isEmpty()) {
+ throw new SecurityException(UNAUTHORIZED_MSG + expr.value());
+ }
+ return Optional.of(buildConstraintExpression(expr.name(), matching));
+ }
+ // No specific constraints: reconstruct (originalText may have old operator)
+ return Optional.of(
+ expr.withOperatorAndValue(LabelPatternUtils.REGEX_MATCH_OPERATOR, expr.value()));
+ }
+
+ // --- Regex Not Match (!~) ---
+
+ private static Optional handleRegexNotMatch(
+ LabelExpression expr, Set allowedValues, boolean hasSpecificConstraints) {
+ if (allowedValues == null) {
+ // No constraints: reconstruct (originalText may have old operator)
+ return Optional.of(
+ expr.withOperatorAndValue(LabelPatternUtils.REGEX_NOT_MATCH_OPERATOR, expr.value()));
+ }
+ if (allowedValues.isEmpty()) {
+ return Optional.empty();
+ }
+ if (hasSpecificConstraints) {
+ Set remaining =
+ allowedValues.stream()
+ .filter(v -> !matchesRegex(expr.value(), v))
+ .collect(Collectors.toSet());
+ if (remaining.isEmpty()) {
+ throw new SecurityException(UNAUTHORIZED_MSG + expr.value());
+ }
+ return Optional.of(buildConstraintExpression(expr.name(), remaining));
+ }
+ // Wildcard constraints: preserve original
+ return Optional.of(
+ expr.withOperatorAndValue(LabelPatternUtils.REGEX_NOT_MATCH_OPERATOR, expr.value()));
+ }
+
+ // ---------------------------------------------------------------------------
+ // Helpers
+ // ---------------------------------------------------------------------------
+
+ /**
+ * Checks if any value in the set is a wildcard — either an exact wildcard pattern (*, .*, .+) or
+ * a glob-style pattern containing * (e.g., *order-service).
+ */
+ private static boolean hasAnyWildcard(Set allowedValues) {
+ return allowedValues.stream()
+ .anyMatch(
+ v ->
+ LabelPatternUtils.isWildcardPattern(v)
+ || v.contains(LabelPatternUtils.WILDCARD_ASTERISK));
+ }
+
+ private static Optional expandWildcard(
+ LabelExpression expr, Set allowedValues) {
+ if (allowedValues == null || allowedValues.isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.of(buildConstraintExpression(expr.name(), allowedValues));
+ }
+
+ private static Optional handleWildcardPattern(
+ LabelExpression expr, Set allowedValues) {
+ if (allowedValues == null || allowedValues.isEmpty()) {
+ return Optional.of(expr.withOperatorAndValue("=~", LabelPatternUtils.REGEX_ANY_CHARS));
+ }
+ return Optional.of(buildConstraintExpression(expr.name(), allowedValues));
+ }
+
+ /**
+ * Builds a constraint expression for missing labels (uses =~ operator by default). Constraint
+ * values can encode operators as prefixes (e.g., "!~^kube-.*" → operator !~, value ^kube-.*).
+ */
+ private static LabelExpression buildMissingConstraintExpression(
+ String labelName, Set allowedValues) {
+ if (allowedValues.size() == 1) {
+ String value = allowedValues.iterator().next();
+ String[] parsed = extractOperatorPrefix(value);
+ if (parsed != null) {
+ return new LabelExpression(labelName, parsed[0], parsed[1], true, null);
+ }
+ return new LabelExpression(labelName, "=~", value, true, null);
+ }
+ String pattern =
+ allowedValues.stream()
+ .map(
+ v -> {
+ if (LabelPatternUtils.isFullRegexPattern(v)
+ || v.contains(LabelPatternUtils.REGEX_ANY_CHARS)
+ || v.contains(LabelPatternUtils.REGEX_ONE_OR_MORE)) {
+ return v;
+ }
+ return v;
+ })
+ .collect(Collectors.joining("|"));
+ return new LabelExpression(labelName, "=~", pattern, true, null);
+ }
+
+ /**
+ * Builds a constraint expression from operator handling (uses = for single values, =~ for
+ * multiple). Constraint values can encode operators as prefixes.
+ */
+ private static LabelExpression buildConstraintExpression(
+ String labelName, Set allowedValues) {
+ if (allowedValues.size() == 1) {
+ String value = allowedValues.iterator().next();
+ String[] parsed = extractOperatorPrefix(value);
+ if (parsed != null) {
+ return new LabelExpression(labelName, parsed[0], parsed[1], true, null);
+ }
+ if (LabelPatternUtils.isWildcardPattern(value)) {
+ return new LabelExpression(
+ labelName, "=~", LabelPatternUtils.convertWildcardToRegex(value), true, null);
+ }
+ return new LabelExpression(labelName, "=", value, true, null);
+ }
+ String pattern =
+ allowedValues.stream()
+ .map(
+ v -> {
+ if (LabelPatternUtils.isFullRegexPattern(v)
+ || v.contains(LabelPatternUtils.REGEX_ANY_CHARS)
+ || v.contains(LabelPatternUtils.REGEX_ONE_OR_MORE)) {
+ return v;
+ }
+ return v;
+ })
+ .collect(Collectors.joining("|"));
+ return new LabelExpression(labelName, "=~", pattern, true, null);
+ }
+
+ /**
+ * Extracts an operator prefix from a constraint value. Constraint values can encode operators as
+ * prefixes, e.g., "!~^kube-.*" → ["!~", "^kube-.*"].
+ *
+ * @return [operator, value] if prefix found, null otherwise
+ */
+ private static String[] extractOperatorPrefix(String value) {
+ if (value != null) {
+ for (String prefix : VALUE_OPERATOR_PREFIXES) {
+ if (value.startsWith(prefix)) {
+ return new String[] {prefix, value.substring(prefix.length())};
+ }
+ }
+ }
+ return null;
+ }
+
+ private static boolean matchesRegex(String regexPattern, String valueToTest) {
+ try {
+ Pattern pattern = Pattern.compile(regexPattern);
+ return pattern.matcher(valueToTest).matches();
+ } catch (PatternSyntaxException _) {
+ return regexPattern.contains(valueToTest) || valueToTest.contains(regexPattern);
+ }
+ }
+
+ private static boolean isValid(LabelExpression expr, Map> constraints) {
+ if (expr.passthrough()) {
+ return true;
+ }
+
+ // Regex and not-match operators skip value validation
+ String op = expr.operator();
+ if (op.contains("~") || "!=".equals(op)) {
+ return true;
+ }
+
+ // Validate specific values against constraints
+ Matcher matcher = LabelPatternUtils.LABEL_MATCHER_PATTERN.matcher(expr.serialize());
+ if (matcher.find()) {
+ String labelName = matcher.group(1);
+ String labelValue = matcher.group(3);
+ boolean allowed =
+ LabelAccessValidator.isLabelValueAccessAllowed(constraints, labelName, labelValue);
+ log.debug(
+ "LabelEnhancer: validate '{}' value='{}' allowed={}", labelName, labelValue, allowed);
+ return allowed;
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/com/evoila/janus/common/enforcement/label/LabelNormalizer.java b/src/main/java/com/evoila/janus/common/enforcement/label/LabelNormalizer.java
new file mode 100644
index 0000000..a22d7a3
--- /dev/null
+++ b/src/main/java/com/evoila/janus/common/enforcement/label/LabelNormalizer.java
@@ -0,0 +1,109 @@
+package com.evoila.janus.common.enforcement.label;
+
+import com.evoila.janus.common.enforcement.model.dto.LabelExpression;
+import com.evoila.janus.common.enforcement.utils.LabelPatternUtils;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Normalizes parsed {@link LabelExpression} objects before enforcement.
+ *
+ * Handles wildcard patterns, empty strings, and operator conversions directly on the data
+ * structure, eliminating the need for string round-trips and re-parsing.
+ */
+@Slf4j
+public final class LabelNormalizer {
+
+ private LabelNormalizer() {}
+
+ /**
+ * Normalizes a list of label expressions.
+ *
+ *
Transformations applied:
+ *
+ *
+ * - Empty strings with {@code =} operator → single allowed value or {@code .+} pattern
+ *
- Wildcard patterns ({@code *}, {@code .*}) → regex operator with proper pattern
+ *
- Preserves {@code !=} and {@code !~} with empty strings as-is
+ *
+ *
+ * @param expressions The parsed expressions to normalize
+ * @param constraints The security constraints (label name → allowed values)
+ * @return New list with normalized expressions
+ */
+ public static List normalize(
+ List expressions, Map> constraints) {
+ return expressions.stream().map(expr -> normalizeSingle(expr, constraints)).toList();
+ }
+
+ static LabelExpression normalizeSingle(
+ LabelExpression expr, Map> constraints) {
+ // Passthrough expressions (intrinsics, keywords) are never normalized
+ if (expr.passthrough()) {
+ return expr;
+ }
+
+ // Skip configuration keys
+ if (!shouldProcessLabel(expr.name())) {
+ return expr;
+ }
+
+ if (!shouldNormalize(expr)) {
+ return expr;
+ }
+
+ return processNormalization(expr, constraints);
+ }
+
+ private static boolean shouldProcessLabel(String labelName) {
+ return labelName != null && !LabelPatternUtils.CONFIGURATION_KEYS.contains(labelName);
+ }
+
+ private static boolean shouldNormalize(LabelExpression expr) {
+ if (expr.value() == null) {
+ return false;
+ }
+ // Don't normalize !~ operators
+ if (LabelPatternUtils.REGEX_NOT_MATCH_OPERATOR.equals(expr.operator())) {
+ return false;
+ }
+ return LabelPatternUtils.isWildcardPattern(expr.value()) || expr.value().isEmpty();
+ }
+
+ private static LabelExpression processNormalization(
+ LabelExpression expr, Map> constraints) {
+ String value = expr.value();
+ String operator = expr.operator();
+
+ // Handle empty string with = operator
+ if (value.isEmpty() && LabelPatternUtils.EQUALS_OPERATOR.equals(operator)) {
+ Set allowedValues = constraints.get(expr.name());
+ if (allowedValues != null && allowedValues.size() == 1) {
+ // Replace with single allowed value
+ String singleValue = allowedValues.iterator().next();
+ log.debug(
+ "Normalized: replaced empty value with single allowed value '{}' for '{}'",
+ singleValue,
+ expr.name());
+ return expr.withValue(singleValue);
+ }
+ // Normalize empty string to =~".+" pattern (operator must change since .+ is regex)
+ log.debug("Normalized: empty value to =~\".+\" for '{}'", expr.name());
+ return expr.withOperatorAndValue(
+ LabelPatternUtils.REGEX_MATCH_OPERATOR, LabelPatternUtils.DEFAULT_WILDCARD_PATTERN);
+ }
+
+ // Handle empty string with != operator - preserve as-is
+ if (value.isEmpty() && LabelPatternUtils.NOT_EQUALS_OPERATOR.equals(operator)) {
+ return expr;
+ }
+
+ // All other cases (wildcard patterns): convert to regex operator with .* pattern
+ String wildcardPattern = LabelPatternUtils.REGEX_ANY_CHARS;
+ log.debug(
+ "Normalized: wildcard '{}' to =~\"{}\" for '{}'", value, wildcardPattern, expr.name());
+ return expr.withOperatorAndValue(LabelPatternUtils.REGEX_MATCH_OPERATOR, wildcardPattern);
+ }
+}
diff --git a/src/main/java/com/evoila/janus/common/enforcement/label/LabelParser.java b/src/main/java/com/evoila/janus/common/enforcement/label/LabelParser.java
new file mode 100644
index 0000000..39c9914
--- /dev/null
+++ b/src/main/java/com/evoila/janus/common/enforcement/label/LabelParser.java
@@ -0,0 +1,168 @@
+package com.evoila.janus.common.enforcement.label;
+
+import com.evoila.janus.common.enforcement.model.dto.LabelExpression;
+import com.evoila.janus.common.enforcement.model.dto.QuerySyntax;
+import com.evoila.janus.common.enforcement.utils.LabelPatternUtils;
+import com.evoila.janus.common.enforcement.utils.StringParser;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Parses label section strings into structured {@link LabelExpression} objects.
+ *
+ * This class is the single entry point for converting raw label strings (e.g.,
+ * "namespace=\"demo\",service=~\"order.*\"") into a list of typed expressions. Parsing happens
+ * exactly once; all subsequent pipeline stages operate on the structured representation.
+ */
+@Slf4j
+public final class LabelParser {
+
+ private LabelParser() {}
+
+ /**
+ * Parses a label section string into a list of {@link LabelExpression} objects.
+ *
+ * @param labels The raw label section (e.g., "namespace=\"demo\",service=\"order\"")
+ * @param syntax The query syntax configuration for language-specific parsing
+ * @return Ordered list of parsed expressions, preserving duplicates
+ */
+ public static List parse(String labels, QuerySyntax syntax) {
+ if (labels == null || labels.trim().isEmpty()) {
+ return List.of();
+ }
+
+ String processed = LabelPatternUtils.fixUrlDecodingIssues(labels);
+
+ // Strip braces if present
+ if (processed.startsWith("{") && processed.endsWith("}")) {
+ processed = processed.substring(1, processed.length() - 1);
+ }
+
+ List pairs = splitPairs(processed, syntax);
+
+ return pairs.stream().map(pair -> parsePair(pair, syntax)).filter(Objects::nonNull).toList();
+ }
+
+ /**
+ * Splits a label section into individual label pair strings.
+ *
+ * @param labels The label section to split
+ * @param syntax The query syntax configuration
+ * @return List of individual label pair strings
+ */
+ static List splitPairs(String labels, QuerySyntax syntax) {
+ if (labels == null || labels.trim().isEmpty()) {
+ return List.of();
+ }
+
+ if (" && ".equals(syntax.separator())) {
+ return splitOnTraceQLSeparator(labels);
+ }
+ return StringParser.parseIntoPairs(labels, ',', false);
+ }
+
+ /**
+ * Parses a single label pair string into a {@link LabelExpression}.
+ *
+ * @param pair The label pair string (e.g., "namespace=\"demo\"")
+ * @param syntax The query syntax configuration
+ * @return The parsed expression, or null if parsing fails
+ */
+ static LabelExpression parsePair(String pair, QuerySyntax syntax) {
+ try {
+ // Check for invalid syntax: ! at the beginning with operators
+ if (pair.startsWith("!") && (pair.contains("=") || pair.contains("~"))) {
+ log.warn("LabelParser: Invalid syntax - ! at beginning with operator: {}", pair);
+ return null;
+ }
+
+ for (String operator : syntax.operatorPrecedence()) {
+ int idx = pair.indexOf(operator);
+ if (idx > -1) {
+ return buildExpression(pair, operator, idx, syntax);
+ }
+ }
+
+ // Check if this is a passthrough keyword (e.g., TraceQL "true", "false")
+ String trimmed = pair.trim();
+ if (syntax.isPassthroughKeyword(trimmed)) {
+ return LabelExpression.passthrough(trimmed);
+ }
+
+ log.warn("LabelParser: No valid operator found in label pair: {}", pair);
+ return null;
+
+ } catch (Exception e) {
+ log.warn("LabelParser: Failed to parse label pair: {}", pair, e);
+ return null;
+ }
+ }
+
+ private static LabelExpression buildExpression(
+ String pair, String operator, int operatorIndex, QuerySyntax syntax) {
+ String name = pair.substring(0, operatorIndex).trim();
+ String rawValue = pair.substring(operatorIndex + operator.length()).trim();
+ boolean quoted = rawValue.startsWith("\"");
+ String value = LabelPatternUtils.extractValue(rawValue, operator);
+
+ // Skip label name validation for intrinsic attributes (e.g. TraceQL's status, duration)
+ if (syntax.isIntrinsicAttribute(name)) {
+ return new LabelExpression(name, operator, value, quoted, pair, true);
+ }
+
+ LabelAccessValidator.validateLabelName(name);
+
+ String resolvedOperator = maybeConvertToRegexOperator(operator, value);
+ return new LabelExpression(name, resolvedOperator, value, quoted, pair);
+ }
+
+ /** Converts = or != to =~ or !~ when the value is a regex pattern. */
+ private static String maybeConvertToRegexOperator(String operator, String value) {
+ if (!LabelPatternUtils.isRegexPattern(value)) {
+ return operator;
+ }
+ if (LabelPatternUtils.EQUALS_OPERATOR.equals(operator)) {
+ log.debug("LabelParser: Converting operator from '=' to '=~' for regex pattern: {}", value);
+ return LabelPatternUtils.REGEX_MATCH_OPERATOR;
+ }
+ if (LabelPatternUtils.NOT_EQUALS_OPERATOR.equals(operator)) {
+ log.debug("LabelParser: Converting operator from '!=' to '!~' for regex pattern: {}", value);
+ return LabelPatternUtils.REGEX_NOT_MATCH_OPERATOR;
+ }
+ return operator;
+ }
+
+ /** Splits a TraceQL labels string on "&&" separator, respecting quoted values. */
+ private static List splitOnTraceQLSeparator(String labels) {
+ List pairs = new ArrayList<>();
+ StringBuilder current = new StringBuilder();
+ boolean inQuotes = false;
+ int i = 0;
+
+ while (i < labels.length()) {
+ char c = labels.charAt(i);
+ if (c == '"' && (i == 0 || labels.charAt(i - 1) != '\\')) {
+ inQuotes = !inQuotes;
+ }
+ if (!inQuotes && c == '&' && i + 1 < labels.length() && labels.charAt(i + 1) == '&') {
+ addNonEmptyPair(pairs, current);
+ current.setLength(0);
+ i += 2;
+ } else {
+ current.append(c);
+ i++;
+ }
+ }
+ addNonEmptyPair(pairs, current);
+ return pairs;
+ }
+
+ private static void addNonEmptyPair(List pairs, StringBuilder current) {
+ String pair = current.toString().trim();
+ if (!pair.isEmpty()) {
+ pairs.add(pair);
+ }
+ }
+}
diff --git a/src/main/java/com/evoila/janus/common/enforcement/label/LabelProcessor.java b/src/main/java/com/evoila/janus/common/enforcement/label/LabelProcessor.java
index e8e79e9..0e8b29d 100644
--- a/src/main/java/com/evoila/janus/common/enforcement/label/LabelProcessor.java
+++ b/src/main/java/com/evoila/janus/common/enforcement/label/LabelProcessor.java
@@ -1,58 +1,36 @@
package com.evoila.janus.common.enforcement.label;
-import com.evoila.janus.common.enforcement.core.OperatorHandlers;
-import com.evoila.janus.common.enforcement.model.dto.EnhancementData;
import com.evoila.janus.common.enforcement.model.dto.LabelConstraintInfo;
+import com.evoila.janus.common.enforcement.model.dto.LabelExpression;
import com.evoila.janus.common.enforcement.model.dto.QueryContext;
import com.evoila.janus.common.enforcement.model.dto.QuerySyntax;
import com.evoila.janus.common.enforcement.model.result.EnhancementResult;
-import com.evoila.janus.common.enforcement.utils.LabelPatternUtils;
import com.evoila.janus.common.enforcement.utils.StringParser;
-import java.util.*;
-import java.util.function.Function;
-import java.util.regex.Matcher;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
/**
- * Static utility class for processing and enhancing label expressions with security constraints.
+ * Facade for the label processing pipeline.
*
- * This class provides comprehensive functionality for: - Parsing label sections from queries
- * (e.g., "namespace='demo',service='*'") - Normalizing wildcard patterns and complex expressions -
- * Applying security constraints to individual labels - Validating label values against allowed
- * constraints - Building enhanced label sections with proper syntax
- *
- *
The processor supports various operators (=, !=, =~, !~) and handles complex scenarios like
- * wildcard patterns, regex expressions, and nested structures.
+ *
Orchestrates the linear flow: Parse → Normalize → Enhance → Validate → Serialize. Delegates
+ * each step to a focused class ({@link LabelParser}, {@link LabelNormalizer}, {@link
+ * LabelEnhancer}).
*/
@Slf4j
public final class LabelProcessor {
- // Static operator strategy factories to avoid recreation
- private static final Map<
- String, Function