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: + * + *

+ */ +@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>, Function>>> - STRATEGY_FACTORIES = - Map.of( - LabelPatternUtils.EQUALS_OPERATOR, labelConstraints -> OperatorHandlers::handleEquals, - LabelPatternUtils.NOT_EQUALS_OPERATOR, - labelConstraints -> OperatorHandlers::handleNotEquals, - LabelPatternUtils.REGEX_MATCH_OPERATOR, - labelConstraints -> OperatorHandlers::handleRegexMatch, - LabelPatternUtils.REGEX_NOT_MATCH_OPERATOR, - labelConstraints -> OperatorHandlers::handleRegexNotMatch); - - private LabelProcessor() { - // Utility class - prevent instantiation - } + private LabelProcessor() {} /** * Enhances a labels section with security constraints using PromQL/LogQL syntax (default). * - *

Delegates to the language-aware overload with PromQL syntax for backward compatibility. - * * @param existingLabels The existing labels section to enhance - * @param labelConstraints The security constraints to apply (label name -> allowed values) - * @return EnhancementResult containing the enhanced labels section and any added constraints + * @param labelConstraints The security constraints to apply (label name → allowed values) + * @return EnhancementResult containing the enhanced labels section */ public static EnhancementResult enhanceLabelsSection( String existingLabels, Map> labelConstraints) { @@ -65,90 +43,60 @@ public static EnhancementResult enhanceLabelsSection( /** * Enhances a labels section with security constraints using the given query syntax. * - *

This method is the main entry point for label enhancement. It: - Parses existing labels from - * the input string - Normalizes wildcard patterns and complex expressions - Applies security - * constraints to each label - Validates enhanced labels against allowed values - Combines results - * into a final enhanced labels section + *

Main entry point for label enhancement. Executes the pipeline: + * + *

    + *
  1. Parse — string → {@link LabelExpression} list (once, no re-parse) + *
  2. Normalize — wildcards, empty strings, operator conversions on the data structure + *
  3. Enhance — apply security constraints per operator + *
  4. Validate — filter out disallowed values + *
  5. Add missing — append constraint labels not already present + *
  6. Serialize — {@link LabelExpression} list → string (once, at the end) + *
* * @param existingLabels The existing labels section to enhance - * @param labelConstraints The security constraints to apply (label name -> allowed values) + * @param labelConstraints The security constraints to apply (label name → allowed values) * @param syntax The query syntax configuration for language-specific parsing - * @return EnhancementResult containing the enhanced labels section and any added constraints + * @return EnhancementResult containing the enhanced labels section */ public static EnhancementResult enhanceLabelsSection( String existingLabels, Map> labelConstraints, QuerySyntax syntax) { if (existingLabels == null || existingLabels.trim().isEmpty()) { - // No existing labels, build constraints from labelConstraints - return buildConstraintsFromLabelConstraints(labelConstraints, syntax); + // No existing labels — build constraints from scratch + List missing = + LabelEnhancer.addMissingConstraints(List.of(), labelConstraints); + return toResult(missing, syntax); } log.debug("LabelProcessor: Enhancing labels section: '{}'", existingLabels); try { - // Parse the existing labels (list preserves duplicates, map for lookups) - List> parsedEntries = - parseLabelsSectionAsList(existingLabels, syntax); - Map parsedConstraints = - parsedEntries.stream() - .collect( - Collectors.toMap( - Map.Entry::getKey, Map.Entry::getValue, (a, b) -> b, LinkedHashMap::new)); - Set existingLabelNames = parsedConstraints.keySet(); - - // Normalize wildcard patterns - String normalizedLabels = - normalizeWildcardPatterns( - existingLabels, parsedConstraints, existingLabelNames, labelConstraints); + // 1. Parse: String → structured expressions (once, never again) + List expressions = LabelParser.parse(existingLabels, syntax); - // Re-parse after normalization if needed - if (!normalizedLabels.equals(existingLabels)) { - parsedEntries = parseLabelsSectionAsList(normalizedLabels, syntax); - parsedConstraints = - parsedEntries.stream() - .collect( - Collectors.toMap( - Map.Entry::getKey, Map.Entry::getValue, (a, b) -> b, LinkedHashMap::new)); - existingLabelNames = parsedConstraints.keySet(); - } + // 2. Normalize: wildcards, empty strings, pattern conversions on data structure + expressions = LabelNormalizer.normalize(expressions, labelConstraints); - // Process all existing labels in batch (using list to preserve duplicates) - EnhancementResult existingLabelsResult = - processLabelsBatch(parsedEntries, labelConstraints, syntax); + // 3. Enhance: apply security constraints per operator + expressions = LabelEnhancer.enhance(expressions, labelConstraints); - // If processing existing labels failed, return the failure - if (!existingLabelsResult.isSuccess()) { - return existingLabelsResult; + // If no expressions survived enhancement, return failure + if (expressions.isEmpty()) { + log.debug("LabelProcessor: No labels were enhanced, returning failure"); + return EnhancementResult.failure("No labels could be enhanced"); } - // Build additional constraints from labelConstraints (but skip wildcard ones) - EnhancementResult additionalConstraintsResult = - buildConstraintsFromLabelConstraints(labelConstraints, syntax); - - // Combine the results - List allLabels = new ArrayList<>(); - List allAddedConstraints = new ArrayList<>(); - - // Add existing labels - if (!existingLabelsResult.getEnhancedQuery().isEmpty()) { - allLabels.add(existingLabelsResult.getEnhancedQuery()); - } + // 4. Validate: filter out disallowed values + expressions = LabelEnhancer.validate(expressions, labelConstraints); - // Add additional constraints (but only for labels that don't already exist) - if (additionalConstraintsResult.isSuccess() - && !additionalConstraintsResult.getEnhancedQuery().isEmpty()) { - String additionalConstraints = additionalConstraintsResult.getEnhancedQuery(); - // Only add constraints for labels that don't already exist - String filteredAdditionalConstraints = - filterConstraintsForExistingLabels(additionalConstraints, existingLabelNames, syntax); - if (!filteredAdditionalConstraints.isEmpty()) { - allLabels.add(filteredAdditionalConstraints); - } - } + // 5. Add missing: append constraint labels not already present + expressions = LabelEnhancer.addMissingConstraints(expressions, labelConstraints); - String finalLabels = String.join(syntax.separator(), allLabels); + // 6. Serialize: structured expressions → string (once, at the end) + String finalLabels = serialize(expressions, syntax); log.debug("LabelProcessor: Final enhanced labels section: '{}'", finalLabels); - return EnhancementResult.success(finalLabels, allAddedConstraints); + return EnhancementResult.success(finalLabels, List.of()); } catch (SecurityException e) { throw e; @@ -158,617 +106,73 @@ public static EnhancementResult enhanceLabelsSection( } } - /** Filters out constraints for labels that already exist */ - private static String filterConstraintsForExistingLabels( - String additionalConstraints, Set existingLabelNames, QuerySyntax syntax) { - if (additionalConstraints.isEmpty()) { - return ""; - } - - List filteredConstraints = new ArrayList<>(); - List constraintParts = parseLabelPairs(additionalConstraints, syntax); - - for (String constraint : constraintParts) { - String labelName = extractLabelNameFromConstraint(constraint); - if (labelName != null && !existingLabelNames.contains(labelName)) { - filteredConstraints.add(constraint); - } - } - - return String.join(syntax.separator(), filteredConstraints); - } - - /** Extracts the label name from a constraint string */ - private static String extractLabelNameFromConstraint(String constraint) { - if (constraint == null || constraint.trim().isEmpty()) { - return null; - } - - String trimmed = constraint.trim(); - int operatorIndex = -1; - - // Find the first operator - for (int i = 0; i < trimmed.length(); i++) { - char c = trimmed.charAt(i); - if (c == '=' || c == '!' || c == '~') { - operatorIndex = i; - break; - } - } - - if (operatorIndex > 0) { - return trimmed.substring(0, operatorIndex).trim(); - } - - return null; - } - - /** Builds constraints from labelConstraints when there are no existing labels */ - private static EnhancementResult buildConstraintsFromLabelConstraints( - Map> labelConstraints, QuerySyntax syntax) { - List builtConstraints = new ArrayList<>(); - - for (Map.Entry> entry : labelConstraints.entrySet()) { - String labelName = entry.getKey(); - Set allowedValues = entry.getValue(); - - // Skip configuration keys (e.g., "labels", "groups") — these are not Prometheus labels - if (LabelPatternUtils.CONFIGURATION_KEYS.contains(labelName) || "labels".equals(labelName)) { - continue; - } - - // Skip wildcard constraints - if (allowedValues != null - && !allowedValues.isEmpty() - && !LabelPatternUtils.containsWildcardValues(allowedValues)) { - - // Build constraint for this label - String constraint = buildConstraintFromValues(labelName, allowedValues); - builtConstraints.add(constraint); - } - } - - String finalConstraints = String.join(syntax.separator(), builtConstraints); - log.debug("LabelProcessor: Built constraints from labelConstraints: '{}'", finalConstraints); - - return EnhancementResult.success(finalConstraints, builtConstraints); - } - - /** Builds a constraint string from allowed values */ - private static String buildConstraintFromValues(String labelName, Set allowedValues) { - return LabelPatternUtils.buildConstraintFromValues(labelName, allowedValues); - } - - /** Filters out wildcard constraints that should be skipped */ - private static Map filterOutWildcardConstraints( - Map parsedConstraints) { - // Don't filter out existing parsed constraints - they should all be processed - // The wildcard filtering should only apply when building new constraints - return parsedConstraints; - } - /** - * Processes all labels in a batch to avoid individual enhancer creation - * - * @param parsedEntries List of label entries (preserves duplicate label names) - * @param labelConstraints The security constraints to apply - * @param syntax The query syntax configuration - * @return EnhancementResult with enhanced labels - */ - private static EnhancementResult processLabelsBatch( - List> parsedEntries, - Map> labelConstraints, - QuerySyntax syntax) { - List enhancedLabels = new ArrayList<>(); - List addedConstraints = new ArrayList<>(); - - // Create static operator strategies once - Map>> operatorStrategies = - createStaticOperatorStrategies(labelConstraints); - - for (Map.Entry entry : parsedEntries) { - String labelName = entry.getKey(); - LabelConstraintInfo constraint = entry.getValue(); - - // TraceQL passthrough keywords (e.g. "true", "false"): preserve in original position - if (syntax.isPassthroughKeyword(labelName)) { - enhancedLabels.add(labelName); - continue; - } - - // TraceQL intrinsic attributes: pass through without enforcement - if (syntax.isIntrinsicAttribute(labelName)) { - enhancedLabels.add(preserveOriginalOrReconstruct(constraint)); - continue; - } - - Set allowedValues = labelConstraints.get(labelName); - - // No enforcement constraints defined for this label: preserve original text. - // Exceptions (must still go through the enhancement pipeline): - // - wildcard/empty values need normalization (e.g., service="*" → service=~".*") - // - regex-pattern values trigger operator conversion (= → =~) during parsing, - // so the originalText still has the old operator and must not be used as-is - if (allowedValues == null - && !LabelPatternUtils.isEmptyOrWildcard(constraint.value()) - && !LabelPatternUtils.isRegexPattern(constraint.value())) { - enhancedLabels.add(preserveOriginalOrReconstruct(constraint)); - continue; - } - - log.debug( - "LabelProcessor: Processing label '{}' with constraint: {} and allowed values: {}", - labelName, - constraint, - allowedValues); - - Optional enhancedLabel = - enhanceSingleLabelWithStaticStrategies( - labelName, constraint, allowedValues, operatorStrategies); - - enhancedLabel.ifPresent(enhancedLabels::add); - } - - // If no labels were enhanced, return failure - if (enhancedLabels.isEmpty()) { - log.debug("LabelProcessor: No labels were enhanced, returning failure"); - return EnhancementResult.failure("No labels could be enhanced"); - } - - // Filter and validate labels - List filteredLabels = filterAndValidateLabels(enhancedLabels, labelConstraints); - - String finalLabels = String.join(syntax.separator(), filteredLabels); - log.debug("LabelProcessor: Final enhanced labels section: '{}'", finalLabels); - - return EnhancementResult.success(finalLabels, addedConstraints); - } - - /** Preserves original label pair text if available, otherwise reconstructs from components. */ - private static String preserveOriginalOrReconstruct(LabelConstraintInfo constraint) { - if (constraint.originalText() != null) { - return constraint.originalText(); - } - String q = constraint.quoted() ? "\"" : ""; - return constraint.operator() + q + constraint.value() + q; - } - - /** Creates static operator strategies to avoid recreation */ - private static Map>> - createStaticOperatorStrategies(Map> labelConstraints) { - Map>> strategies = new HashMap<>(); - - STRATEGY_FACTORIES.forEach( - (operator, factory) -> strategies.put(operator, factory.apply(labelConstraints))); - - log.debug("LabelProcessor: Created static operator strategies: {}", strategies.keySet()); - return strategies; - } - - /** Enhances a single label using static operator strategies */ - private static Optional enhanceSingleLabelWithStaticStrategies( - String labelName, - LabelConstraintInfo constraintInfo, - Set allowedValues, - Map>> operatorStrategies) { - String value = constraintInfo.value(); - String operator = constraintInfo.operator(); - - // Pre-compute wildcard checks - boolean isValueWildcard = LabelPatternUtils.isEmptyOrWildcard(value); - boolean hasWildcardConstraints = - allowedValues != null && LabelPatternUtils.containsWildcardValues(allowedValues); - boolean hasSpecificConstraints = - allowedValues != null && !allowedValues.isEmpty() && !hasWildcardConstraints; - - log.debug( - "LabelProcessor: Enhancing label '{}' with operator '{}', value '{}', allowedValues: {}, isValueWildcard: {}, hasWildcardConstraints: {}, hasSpecificConstraints: {}", - labelName, - operator, - value, - allowedValues, - isValueWildcard, - hasWildcardConstraints, - hasSpecificConstraints); - - EnhancementData enhancementData = - new EnhancementData( - labelName, - value, - operator, - allowedValues, - isValueWildcard, - hasWildcardConstraints, - hasSpecificConstraints, - constraintInfo.quoted(), - constraintInfo.originalText()); - - // Handle special cases first, but NOT for !~ operators - if (!LabelPatternUtils.REGEX_NOT_MATCH_OPERATOR.equals(operator) - && LabelPatternUtils.isWildcardPatternForEnhancement(value, operator)) { - log.debug("LabelProcessor: Handling wildcard pattern for enhancement"); - return LabelPatternUtils.handleWildcardPattern(labelName, allowedValues); - } - - // Use static strategy pattern - Function> strategy = operatorStrategies.get(operator); - if (strategy != null) { - log.debug("LabelProcessor: Using strategy for operator: {}", operator); - Optional result = strategy.apply(enhancementData); - log.debug("LabelProcessor: Enhanced label result for '{}': {}", labelName, result); - return result; - } - - log.warn("LabelProcessor: Unknown operator: {}", operator); - return Optional.empty(); - } - - /** Filters and validates enhanced labels */ - private static List filterAndValidateLabels( - List enhancedLabels, Map> labelConstraints) { - log.debug( - "LabelProcessor: Filtering {} enhanced labels: {}", enhancedLabels.size(), enhancedLabels); - - return enhancedLabels.stream() - .filter( - label -> { - // Extract label name and value for validation - Matcher matcher = LabelPatternUtils.LABEL_MATCHER_PATTERN.matcher(label); - if (matcher.find()) { - String labelName = matcher.group(1); - String labelValue = matcher.group(3); // group 3 is the value inside quotes - - // Skip validation for regex patterns - if (label.contains("=~") || label.contains("!~")) { - log.debug("LabelProcessor: Skipping validation for pattern: {}", label); - return true; - } - - // Special handling for != constraints - preserve them as they are user constraints - if (label.contains("!=")) { - log.debug("LabelProcessor: Preserving != constraint: {}", label); - return true; - } - - // Validate specific values - boolean isAllowed = - LabelAccessValidator.isLabelValueAccessAllowed( - labelConstraints, labelName, labelValue); - log.debug( - "LabelProcessor: Label '{}' with value '{}' is allowed: {}", - labelName, - labelValue, - isAllowed); - return isAllowed; - } - return true; // Keep labels that don't match pattern - }) - .toList(); - } - - /** - * Parses a labels section string into a map of label constraints using PromQL/LogQL syntax. - * - * @param labels The labels section to parse - * @return Map of label name to LabelConstraintInfo + * Parses a labels section string into a map of label constraints (PromQL/LogQL syntax). Duplicate + * label names are deduplicated (last wins). */ public static Map parseLabelsSection(String labels) { return parseLabelsSection(labels, QuerySyntax.forLanguage(QueryContext.QueryLanguage.PROMQL)); } /** - * Parses a labels section string into a map of label constraints using the given syntax. Note: - * duplicate label names are deduplicated (last wins). Use {@link #parseLabelsSectionAsList} when - * duplicate labels must be preserved. - * - * @param labels The labels section to parse - * @param syntax The query syntax configuration - * @return Map of label name to LabelConstraintInfo + * Parses a labels section string into a map of label constraints using the given syntax. + * Duplicate label names are deduplicated (last wins). */ public static Map parseLabelsSection( String labels, QuerySyntax syntax) { - List> entries = parseLabelsSectionAsList(labels, syntax); - return entries.stream() + List expressions = LabelParser.parse(labels, syntax); + return expressions.stream() .collect( Collectors.toMap( - Map.Entry::getKey, Map.Entry::getValue, (a, b) -> b, LinkedHashMap::new)); - } - - /** - * Parses a labels section string into an ordered list of label entries, preserving duplicate - * label names (e.g., resource.service.name="grafana" AND resource.service.name != nil). - */ - private static List> parseLabelsSectionAsList( - String labels, QuerySyntax syntax) { - if (labels == null || labels.trim().isEmpty()) { - return List.of(); - } - - // Fix URL decoding issues - String processedLabels = LabelPatternUtils.fixUrlDecodingIssues(labels); - - // Strip braces if present - if (processedLabels.startsWith("{") && processedLabels.endsWith("}")) { - processedLabels = processedLabels.substring(1, processedLabels.length() - 1); - } - - // Parse label pairs properly, respecting quoted values - List labelPairs = parseLabelPairs(processedLabels, syntax); - - return labelPairs.stream() - .map(pair -> parseLabelPair(pair, syntax)) - .filter(Objects::nonNull) - .toList(); + LabelExpression::name, + e -> new LabelConstraintInfo(e.value(), e.operator(), e.quoted(), e.originalText()), + (a, b) -> b, + LinkedHashMap::new)); } - /** - * Parses a labels section into individual label pairs using PromQL/LogQL syntax - * (comma-separated). - * - * @param labels The labels section to parse - * @return List of label pair strings - */ + /** Parses a labels section into individual label pair strings using PromQL/LogQL syntax. */ public static List parseLabelPairs(String labels) { return parseLabelPairs(labels, QuerySyntax.forLanguage(QueryContext.QueryLanguage.PROMQL)); } - /** - * Parses a labels section into individual label pairs using the given syntax. - * - * @param labels The labels section to parse - * @param syntax The query syntax configuration - * @return List of label pair strings - */ + /** Parses a labels section into individual label pair strings using the given syntax. */ public static List parseLabelPairs(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); - } - - /** - * Splits a TraceQL labels string on "&&" separator, respecting quoted values. Handles "&&" with - * or without surrounding spaces (e.g., "a && b", "a&& b", "a &&b", "a&&b"). - * - * @param labels The labels string to split - * @return List of individual label pair strings - */ - 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; // skip past && - } 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); - } - } - - /** - * Parses a single label pair (e.g., "name=value" or "name=~regex") using the given syntax. - * - * @param pair The label pair string to parse - * @param syntax The query syntax configuration for operator precedence - * @return Map entry with label name and constraint info, or null if parsing fails - */ - private static Map.Entry parseLabelPair( - String pair, QuerySyntax syntax) { - try { - // Check for invalid syntax: ! at the beginning with operators - if (pair.startsWith("!") && (pair.contains("=") || pair.contains("~"))) { - log.warn("LabelProcessor: Invalid syntax - ! at beginning with operator: {}", pair); - return null; - } - - for (String operator : syntax.operatorPrecedence()) { - int idx = pair.indexOf(operator); - if (idx > -1) { - return buildLabelEntry(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 Map.entry(trimmed, new LabelConstraintInfo(trimmed, "", false, trimmed)); - } - - log.warn("LabelProcessor: No valid operator found in label pair: {}", pair); - return null; - - } catch (Exception e) { - log.warn("LabelProcessor: Failed to parse label pair: {}", pair, e); - return null; - } - } - - /** Builds a label entry from a parsed operator match within a label pair. */ - private static Map.Entry buildLabelEntry( - 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 = extractValue(rawValue, operator); - - // Skip label name validation for intrinsic attributes (e.g. TraceQL's status, duration) - if (!syntax.isIntrinsicAttribute(name)) { - LabelAccessValidator.validateLabelName(name); - } - - String resolvedOperator = maybeConvertToRegexOperator(operator, value); - return Map.entry(name, new LabelConstraintInfo(value, resolvedOperator, 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( - "LabelProcessor: Converting operator from '=' to '=~' for regex pattern: {}", value); - return LabelPatternUtils.REGEX_MATCH_OPERATOR; - } - if (LabelPatternUtils.NOT_EQUALS_OPERATOR.equals(operator)) { - log.debug( - "LabelProcessor: Converting operator from '!=' to '!~' for regex pattern: {}", value); - return LabelPatternUtils.REGEX_NOT_MATCH_OPERATOR; - } - return operator; + return LabelParser.splitPairs(labels, syntax); } /** - * Extracts and processes the value part of a label pair - * - * @param valuePart The value part of the label pair - * @param operator The operator used - * @return The processed value + * Normalizes wildcard patterns in a labels section. Kept for backward compatibility with existing + * tests that call this method directly. */ - private static String extractValue(String valuePart, String operator) { - return LabelPatternUtils.extractValue(valuePart, operator); - } - - /** Normalizes wildcard patterns in a labels section */ public static String normalizeWildcardPatterns( String labels, Map parsedConstraints, Set existingLabelNames, Map> labelConstraints) { - String normalized = labels; - - for (String labelName : existingLabelNames) { - if (shouldProcessLabel(labelName)) { - LabelConstraintInfo constraint = parsedConstraints.get(labelName); - if (shouldNormalizeConstraint(constraint)) { - normalized = - processConstraintNormalization(normalized, labelName, constraint, labelConstraints); - } - } - } - - return normalized; + // Convert to LabelExpression, normalize, serialize back + QuerySyntax syntax = QuerySyntax.forLanguage(QueryContext.QueryLanguage.PROMQL); + List expressions = LabelParser.parse(labels, syntax); + List normalized = LabelNormalizer.normalize(expressions, labelConstraints); + return serialize(normalized, syntax); } - /** Determines if a label should be processed for normalization */ - private static boolean shouldProcessLabel(String labelName) { - return labelName != null && !LabelPatternUtils.CONFIGURATION_KEYS.contains(labelName); - } - - /** Determines if a constraint should be normalized */ - private static boolean shouldNormalizeConstraint(LabelConstraintInfo constraint) { - return constraint != null - && constraint.value() != null - && !LabelPatternUtils.REGEX_NOT_MATCH_OPERATOR.equals(constraint.operator()) - && (LabelPatternUtils.isWildcardPattern(constraint.value()) - || constraint.value().isEmpty()); - } - - /** Processes constraint normalization for a specific label */ - private static String processConstraintNormalization( - String normalized, - String labelName, - LabelConstraintInfo constraint, - Map> labelConstraints) { - // Handle empty string with = operator - if (constraint.value() != null - && constraint.value().isEmpty() - && LabelPatternUtils.EQUALS_OPERATOR.equals(constraint.operator())) { - Set allowedValues = labelConstraints.get(labelName); - if (shouldReplaceWithSingleValue(allowedValues)) { - return replaceWithSingleAllowedValue(normalized, labelName, constraint, allowedValues); - } - return normalizeToWildcardPattern(normalized, labelName, constraint); - } - - // Handle empty string with != operator (preserve as-is, e.g. namespace!="") - if (constraint.value() != null - && constraint.value().isEmpty() - && LabelPatternUtils.NOT_EQUALS_OPERATOR.equals(constraint.operator())) { - return normalized; - } - - // All other cases (including =~"") normalize to wildcard pattern - return normalizeToWildcardPattern(normalized, labelName, constraint); - } - - /** Determines if constraint should be replaced with a single value */ - private static boolean shouldReplaceWithSingleValue(Set allowedValues) { - return allowedValues != null && allowedValues.size() == 1; - } - - /** Replaces constraint with single allowed value */ - private static String replaceWithSingleAllowedValue( - String normalized, - String labelName, - LabelConstraintInfo constraint, - Set allowedValues) { - String singleValue = allowedValues.iterator().next(); - String pattern = labelName + constraint.operator() + "\"" + constraint.value() + "\""; - String replacement = labelName + "=\"" + singleValue + "\""; - - log.debug("Replaced with single allowed value: '{}' -> '{}'", pattern, replacement); - return normalized.replace(pattern, replacement); + /** Checks if a query has label selectors. */ + public static boolean hasLabelSelectors(String query) { + return query != null && !StringParser.findLabelSections(query).isEmpty(); } - /** Normalizes to a wildcard pattern */ - private static String normalizeToWildcardPattern( - String normalized, String labelName, LabelConstraintInfo constraint) { - String pattern = labelName + constraint.operator() + "\"" + constraint.value() + "\""; - - // For empty strings with = operator, preserve the = operator and use .+ - // For other wildcard patterns, convert to =~ operator and use .* - String wildcardPattern = - constraint.value() != null - && constraint.value().isEmpty() - && LabelPatternUtils.EQUALS_OPERATOR.equals(constraint.operator()) - ? LabelPatternUtils.DEFAULT_WILDCARD_PATTERN - : LabelPatternUtils.REGEX_ANY_CHARS; - - // Preserve the original operator for empty strings with = operator - String operator = - constraint.value() != null - && constraint.value().isEmpty() - && LabelPatternUtils.EQUALS_OPERATOR.equals(constraint.operator()) - ? constraint.operator() - : "=~"; - - String replacement = labelName + operator + "\"" + wildcardPattern + "\""; + // --------------------------------------------------------------------------- + // Internal helpers + // --------------------------------------------------------------------------- - log.debug("Normalized to wildcard pattern: '{}' -> '{}'", pattern, replacement); - return normalized.replace(pattern, replacement); + private static String serialize(List expressions, QuerySyntax syntax) { + return expressions.stream() + .map(LabelExpression::serialize) + .collect(Collectors.joining(syntax.separator())); } - /** - * Checks if a query has label selectors - * - * @param query The query to check - * @return true if the query contains label selectors - */ - public static boolean hasLabelSelectors(String query) { - return query != null && !StringParser.findLabelSections(query).isEmpty(); + private static EnhancementResult toResult(List expressions, QuerySyntax syntax) { + String result = serialize(expressions, syntax); + return EnhancementResult.success(result, List.of()); } } diff --git a/src/main/java/com/evoila/janus/common/enforcement/model/dto/EnhancementData.java b/src/main/java/com/evoila/janus/common/enforcement/model/dto/EnhancementData.java deleted file mode 100644 index 62596f3..0000000 --- a/src/main/java/com/evoila/janus/common/enforcement/model/dto/EnhancementData.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.evoila.janus.common.enforcement.model.dto; - -import java.util.Set; - -/** - * Data class for enhancement operations containing all information needed for label processing. - * - *

This record encapsulates complete label enhancement context: - labelName: The name of the - * label being processed - value: The current value of the label - operator: The operator being - * applied (=, !=, =~, !~) - allowedValues: Set of values allowed by security constraints - - * isValueWildcard: Whether the current value is a wildcard - hasWildcardConstraints: Whether - * constraints contain wildcards - hasSpecificConstraints: Whether specific constraints are defined - * - *

Used by OperatorHandlers to make enhancement decisions based on the complete context of the - * label and its constraints. - */ -public record EnhancementData( - String labelName, - String value, - String operator, - Set allowedValues, - boolean isValueWildcard, - boolean hasWildcardConstraints, - boolean hasSpecificConstraints, - boolean quoted, - String originalText) { - - /** - * Returns the original text if available, otherwise reconstructs the label expression from its - * components. Use this in pass-through branches where the label is not being modified. - */ - public String originalOrReconstructed() { - if (originalText != null) { - return originalText; - } - String q = quoted ? "\"" : ""; - return labelName + operator + q + value + q; - } -} diff --git a/src/main/java/com/evoila/janus/common/enforcement/model/dto/LabelExpression.java b/src/main/java/com/evoila/janus/common/enforcement/model/dto/LabelExpression.java new file mode 100644 index 0000000..1d5f599 --- /dev/null +++ b/src/main/java/com/evoila/janus/common/enforcement/model/dto/LabelExpression.java @@ -0,0 +1,63 @@ +package com.evoila.janus.common.enforcement.model.dto; + +/** + * Unified intermediate representation for a single label expression in a query. + * + *

Carries a label through the entire processing pipeline (parse → normalize → enhance → validate + * → serialize) without requiring string round-trips between stages. + * + * @param name The label name (e.g., "namespace", "resource.service.name") + * @param operator The operator (e.g., "=", "!=", "=~", "!~", "<", ">") + * @param value The label value (e.g., "demo", ".*", "order-service") + * @param quoted Whether the value was originally quoted + * @param originalText The original unparsed text, used for pass-through when no modification is + * needed. Set to null when the expression has been modified. + * @param passthrough If true, this expression is preserved as-is without enforcement (e.g., TraceQL + * intrinsics or passthrough keywords like "true", "false") + */ +public record LabelExpression( + String name, + String operator, + String value, + boolean quoted, + String originalText, + boolean passthrough) { + + /** Creates a standard label expression from parsed components. */ + public LabelExpression( + String name, String operator, String value, boolean quoted, String originalText) { + this(name, operator, value, quoted, originalText, false); + } + + /** Creates a passthrough expression that will not be enforced. */ + public static LabelExpression passthrough(String originalText) { + return new LabelExpression(originalText, "", "", false, originalText, true); + } + + /** + * Serializes the expression back to string form. If nothing was modified (originalText is + * present), the original text is returned to preserve formatting. + */ + public String serialize() { + if (originalText != null) { + return originalText; + } + String q = quoted ? "\"" : ""; + return name + operator + q + value + q; + } + + /** Returns a copy with a new operator and value. Clears originalText to force serialization. */ + public LabelExpression withOperatorAndValue(String newOperator, String newValue) { + return new LabelExpression(name, newOperator, newValue, quoted, null, passthrough); + } + + /** Returns a copy with a new value. Clears originalText to force serialization. */ + public LabelExpression withValue(String newValue) { + return new LabelExpression(name, operator, newValue, quoted, null, passthrough); + } + + /** Returns a copy with a new operator. Clears originalText to force serialization. */ + public LabelExpression withOperator(String newOperator) { + return new LabelExpression(name, newOperator, value, quoted, null, passthrough); + } +} diff --git a/src/test/java/com/evoila/janus/common/enforcement/strategy/LabelProcessorTest.java b/src/test/java/com/evoila/janus/common/enforcement/strategy/LabelProcessorTest.java index c61b01b..4771e5b 100644 --- a/src/test/java/com/evoila/janus/common/enforcement/strategy/LabelProcessorTest.java +++ b/src/test/java/com/evoila/janus/common/enforcement/strategy/LabelProcessorTest.java @@ -387,9 +387,9 @@ void testEmptyStringReplacementWithMultipleAllowedValues() { LabelProcessor.normalizeWildcardPatterns( labels, parsedConstraints, existingLabelNames, labelConstraints); - // Then: Should normalize to .+ pattern since there are multiple allowed values + // Then: Should normalize to =~".+" since .+ is a regex pattern assertNotNull(result); - assertEquals("namespace=\".+\"", result); + assertEquals("namespace=~\".+\"", result); } @Test @@ -524,10 +524,12 @@ void testNormalizeEmptyStringWithEqualsOperator() { LabelProcessor.normalizeWildcardPatterns( labels, parsedConstraints, existingLabelNames, labelConstraints); - // Then: Should normalize to .+ since it's not a != or !~ operator + // Then: Should normalize to =~".+" since .+ is a regex pattern assertNotNull(result); assertEquals( - "namespace=\".+\"", result, "Empty string with = operator should be normalized to .+"); + "namespace=~\".+\"", + result, + "Empty string with = operator should be normalized to =~\".+\""); } // ======================================================================== @@ -700,4 +702,68 @@ void traceQL_preservesDuplicateLabelConstraints() { "Should preserve resource.service.name != nil: got '" + enhanced + "'"); assertTrue(enhanced.contains("nestedSetParent<0"), "Should preserve intrinsic attribute"); } + + // --------------------------------------------------------------------------- + // Operator prefix extraction from constraint values + // --------------------------------------------------------------------------- + + @Test + @DisplayName( + "Should extract !~ operator prefix from constraint value when adding missing constraints") + void testAddMissingConstraints_WithNegativeRegexOperatorPrefix() { + // Given: constraint value encodes operator prefix "!~^kube-.*" + String existingLabels = "pod=\"my-pod\""; + Map> constraints = new HashMap<>(); + constraints.put("pod", Set.of("my-pod")); + constraints.put("k8s_namespace_name", Set.of("!~^kube-.*")); + + // When + EnhancementResult result = LabelProcessor.enhanceLabelsSection(existingLabels, constraints); + + // Then: should extract !~ as operator, ^kube-.* as value + assertTrue(result.isSuccess()); + String enhanced = result.getEnhancedQuery(); + assertTrue( + enhanced.contains("k8s_namespace_name!~\"^kube-.*\""), + "Should extract !~ operator prefix from value: got '" + enhanced + "'"); + assertFalse( + enhanced.contains("=~\"!~"), + "Should NOT wrap operator prefix in value: got '" + enhanced + "'"); + } + + @Test + @DisplayName( + "Should extract =~ operator prefix from constraint value when adding missing constraints") + void testAddMissingConstraints_WithRegexOperatorPrefix() { + String existingLabels = "pod=\"my-pod\""; + Map> constraints = new HashMap<>(); + constraints.put("pod", Set.of("my-pod")); + constraints.put("k8s_namespace_name", Set.of("=~prod-.*")); + + EnhancementResult result = LabelProcessor.enhanceLabelsSection(existingLabels, constraints); + + assertTrue(result.isSuccess()); + String enhanced = result.getEnhancedQuery(); + assertTrue( + enhanced.contains("k8s_namespace_name=~\"prod-.*\""), + "Should extract =~ operator prefix from value: got '" + enhanced + "'"); + } + + @Test + @DisplayName( + "Should extract != operator prefix from constraint value when adding missing constraints") + void testAddMissingConstraints_WithNotEqualsOperatorPrefix() { + String existingLabels = "pod=\"my-pod\""; + Map> constraints = new HashMap<>(); + constraints.put("pod", Set.of("my-pod")); + constraints.put("k8s_namespace_name", Set.of("!=kube-system")); + + EnhancementResult result = LabelProcessor.enhanceLabelsSection(existingLabels, constraints); + + assertTrue(result.isSuccess()); + String enhanced = result.getEnhancedQuery(); + assertTrue( + enhanced.contains("k8s_namespace_name!=\"kube-system\""), + "Should extract != operator prefix from value: got '" + enhanced + "'"); + } }