diff --git a/policy/src/main/java/dev/cel/policy/BUILD.bazel b/policy/src/main/java/dev/cel/policy/BUILD.bazel index 916f16f9b..a7bb90ffe 100644 --- a/policy/src/main/java/dev/cel/policy/BUILD.bazel +++ b/policy/src/main/java/dev/cel/policy/BUILD.bazel @@ -247,19 +247,19 @@ java_library( visibility = ["//visibility:private"], deps = [ ":compiled_rule", - "//:auto_value", "//bundle:cel", "//common:cel_ast", "//common:compiler_common", "//common:mutable_ast", + "//common:mutable_source", "//common:operator", "//common/ast", + "//common/ast:mutable_expr", "//common/formats:value_string", "//common/navigation:mutable_navigation", "//extensions:optional_library", "//optimizer:ast_optimizer", "//optimizer:mutable_ast", - "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) diff --git a/policy/src/main/java/dev/cel/policy/RuleComposer.java b/policy/src/main/java/dev/cel/policy/RuleComposer.java index 7bbde7685..cafef4b7c 100644 --- a/policy/src/main/java/dev/cel/policy/RuleComposer.java +++ b/policy/src/main/java/dev/cel/policy/RuleComposer.java @@ -18,15 +18,17 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static java.util.stream.Collectors.toCollection; -import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import dev.cel.bundle.Cel; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelMutableAst; +import dev.cel.common.CelMutableSource; import dev.cel.common.CelValidationException; import dev.cel.common.Operator; +import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import dev.cel.common.ast.CelMutableExpr; import dev.cel.common.formats.ValueString; import dev.cel.common.navigation.CelNavigableMutableAst; import dev.cel.common.navigation.CelNavigableMutableExpr; @@ -48,22 +50,11 @@ final class RuleComposer implements CelAstOptimizer { @Override public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) { - RuleOptimizationResult result = optimizeRule(cel, compiledRule); - return OptimizationResult.create(result.ast().toParsedAst()); + Step result = optimizeRule(cel, compiledRule); + return OptimizationResult.create(result.expr.toParsedAst()); } - @AutoValue - abstract static class RuleOptimizationResult { - abstract CelMutableAst ast(); - - abstract boolean isOptionalResult(); - - static RuleOptimizationResult create(CelMutableAst ast, boolean isOptionalResult) { - return new AutoValue_RuleComposer_RuleOptimizationResult(ast, isOptionalResult); - } - } - - private RuleOptimizationResult optimizeRule(Cel cel, CelCompiledRule compiledRule) { + private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) { cel = cel.toCelBuilder() .addVarDeclarations( @@ -72,81 +63,57 @@ private RuleOptimizationResult optimizeRule(Cel cel, CelCompiledRule compiledRul .collect(toImmutableList())) .build(); - CelMutableAst matchAst = astMutator.newGlobalCall(Function.OPTIONAL_NONE.getFunction()); - boolean isOptionalResult = true; - // Keep track of the last output ID that might cause type-check failure while attempting to - // compose the subgraphs. + Step output = null; + // If the rule has an optional output, the last result in the ternary should return + // `optional.none`. This output is implicit and created here to reflect the desired + // last possible output of this type of rule. + if (compiledRule.hasOptionalOutput()) { + output = + Step.newUnconditionalOptionalStep( + newTrueLiteral(), astMutator.newGlobalCall(Function.OPTIONAL_NONE.getFunction())); + } + long lastOutputId = 0; for (CelCompiledMatch match : Lists.reverse(compiledRule.matches())) { CelAbstractSyntaxTree conditionAst = match.condition(); - // If the condition is trivially true, none of the matches in the rule causes the result - // to become optional, and the rule is not the last match, then this will introduce - // unreachable outputs or rules. boolean isTriviallyTrue = match.isConditionTriviallyTrue(); + CelMutableAst condAst = CelMutableAst.fromCelAst(conditionAst); switch (match.result().kind()) { - // For the match's output, determine whether the output should be wrapped - // into an optional value, a conditional, or both. case OUTPUT: + // If the match has an output, then it is considered a non-optional output since + // it is explicitly stated. If the rule itself is optional, then the base case value + // of output being optional.none() will convert the non-optional value to an optional + // one. OutputValue matchOutput = match.result().output(); CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast()); - if (isTriviallyTrue) { - matchAst = outAst; - isOptionalResult = false; - lastOutputId = matchOutput.sourceId(); - continue; - } - if (isOptionalResult) { - outAst = astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), outAst); - } - - matchAst = - astMutator.newGlobalCall( - Operator.CONDITIONAL.getFunction(), - CelMutableAst.fromCelAst(conditionAst), - outAst, - matchAst); + Step step = Step.newNonOptionalStep(!isTriviallyTrue, condAst, outAst); + output = combine(astMutator, step, output); + assertComposedAstIsValid( cel, - matchAst, + output.expr, "conflicting output types found.", matchOutput.sourceId(), lastOutputId); lastOutputId = matchOutput.sourceId(); - continue; + break; case RULE: // If the match has a nested rule, then compute the rule and whether it has // an optional return value. CelCompiledRule matchNestedRule = match.result().rule(); - RuleOptimizationResult nestedRule = optimizeRule(cel, matchNestedRule); + Step nestedRule = optimizeRule(cel, matchNestedRule); boolean nestedHasOptional = matchNestedRule.hasOptionalOutput(); - CelMutableAst nestedRuleAst = nestedRule.ast(); - if (isOptionalResult && !nestedHasOptional) { - nestedRuleAst = - astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), nestedRuleAst); - } - if (!isOptionalResult && nestedHasOptional) { - matchAst = astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), matchAst); - isOptionalResult = true; - } - // If either the nested rule or current condition output are optional then - // use optional.or() to specify the combination of the first and second results - // Note, the argument order is reversed due to the traversal of matches in - // reverse order. - if (isOptionalResult && isTriviallyTrue) { - matchAst = astMutator.newMemberCall(nestedRuleAst, Function.OR.getFunction(), matchAst); - } else { - matchAst = - astMutator.newGlobalCall( - Operator.CONDITIONAL.getFunction(), - CelMutableAst.fromCelAst(conditionAst), - nestedRuleAst, - matchAst); - } + + Step ruleStep = + nestedHasOptional + ? Step.newOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr) + : Step.newNonOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr); + output = combine(astMutator, ruleStep, output); assertComposedAstIsValid( cel, - matchAst, + output.expr, String.format( "failed composing the subrule '%s' due to conflicting output types.", matchNestedRule.ruleId().map(ValueString::value).orElse("")), @@ -155,11 +122,124 @@ private RuleOptimizationResult optimizeRule(Cel cel, CelCompiledRule compiledRul } } - CelMutableAst result = inlineCompiledVariables(matchAst, compiledRule.variables()); + CelMutableAst resultExpr = output.expr; + resultExpr = inlineCompiledVariables(resultExpr, compiledRule.variables()); + resultExpr = astMutator.renumberIdsConsecutively(resultExpr); + + return output.isOptional + ? Step.newUnconditionalOptionalStep(newTrueLiteral(), resultExpr) + : Step.newUnconditionalNonOptionalStep(newTrueLiteral(), resultExpr); + } + + static RuleComposer newInstance( + CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) { + return new RuleComposer(compiledRule, variablePrefix, iterationLimit); + } + + // Assembles two output expressions into a single output step. + private Step combine(AstMutator astMutator, Step currentStep, Step accumulatedStep) { + if (accumulatedStep == null) { + return currentStep; + } + CelMutableAst trueCondition = newTrueLiteral(); + + if (currentStep.isOptional) { + return combineWhenCurrentIsOptional(currentStep, accumulatedStep, astMutator, trueCondition); + } else { + return combineWhenCurrentIsNonOptional( + currentStep, accumulatedStep, astMutator, trueCondition); + } + } + + private Step combineWhenCurrentIsOptional( + Step currentStep, Step accumulatedStep, AstMutator astMutator, CelMutableAst trueCondition) { + // optional.combine(optional) // optional + // (optional && conditional).combine(non-optional) // optional + // (optional && unconditional).combine(non-optional) // non-optional + if (accumulatedStep.isOptional) { + if (currentStep.isConditional) { + return Step.newUnconditionalOptionalStep( + trueCondition, + astMutator.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + currentStep.cond, + currentStep.expr, + accumulatedStep.expr)); + } else { + if (!isOptionalNone(accumulatedStep.expr)) { + // If either the nested rule or current condition output are optional then + // use optional.or() to specify the combination of the first and second results + // Note, the argument order is reversed due to the traversal of matches in + // reverse order. + return Step.newUnconditionalOptionalStep( + trueCondition, + astMutator.newMemberCall(currentStep.expr, "or", accumulatedStep.expr)); + } + return currentStep; + } + } else { // accumulatedStep is non-optional + if (currentStep.isConditional) { + return Step.newUnconditionalOptionalStep( + trueCondition, + astMutator.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + currentStep.cond, + currentStep.expr, + astMutator.newGlobalCall( + Function.OPTIONAL_OF.getFunction(), accumulatedStep.expr))); + } else { + return Step.newUnconditionalNonOptionalStep( + trueCondition, + astMutator.newMemberCall(currentStep.expr, "orValue", accumulatedStep.expr)); + } + } + } + + private Step combineWhenCurrentIsNonOptional( + Step currentStep, Step accumulatedStep, AstMutator astMutator, CelMutableAst trueCondition) { + // non-optional.combine(non-optional) // non-optional + // (non-optional && conditional).combine(optional) // optional + // (non-optional && unconditional).combine(optional) // non-optional + // + // The last combination case is unusual, but effectively it means that the non-optional value + // prunes away + // the potential optional output. + if (accumulatedStep.isOptional) { + if (currentStep.isConditional) { + return Step.newUnconditionalOptionalStep( + trueCondition, + astMutator.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + currentStep.cond, + astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), currentStep.expr), + accumulatedStep.expr)); + } else { + // If the condition is trivially true, none of the matches in the rule causes the result + // to become optional, and the rule is not the last match, then this will introduce + // unreachable outputs or rules (pruning away 'accumulatedStep'). + return currentStep; + } + } else { // accumulatedStep is non-optional + return Step.newUnconditionalNonOptionalStep( + trueCondition, + astMutator.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + currentStep.cond, + currentStep.expr, + accumulatedStep.expr)); + } + } - result = astMutator.renumberIdsConsecutively(result); + private static boolean isOptionalNone(CelMutableAst ast) { + CelMutableExpr expr = ast.expr(); + return expr.getKind().equals(Kind.CALL) + && expr.call().function().equals("optional.none") + && expr.call().args().isEmpty(); + } - return RuleOptimizationResult.create(result, isOptionalResult); + private static CelMutableAst newTrueLiteral() { + return CelMutableAst.of( + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), CelMutableSource.newInstance()); } private CelMutableAst inlineCompiledVariables( @@ -186,11 +266,6 @@ private CelMutableAst inlineCompiledVariables( return mutatedAst; } - static RuleComposer newInstance( - CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) { - return new RuleComposer(compiledRule, variablePrefix, iterationLimit); - } - private void assertComposedAstIsValid( Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) { assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids)); @@ -206,10 +281,55 @@ private void assertComposedAstIsValid( } } - private RuleComposer(CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) { - this.compiledRule = checkNotNull(compiledRule); - this.variablePrefix = variablePrefix; - this.astMutator = AstMutator.newInstance(iterationLimit); + // Step represents an intermediate stage of rule and match expression composition. + // + // The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of + // condition and output expressions, and have no notion of how the order of combination would + // impact composition since composition rules may vary based on the policy execution semantic, + // e.g. first-match versus logical-or, logical-and, or accumulation. + private static class Step { + /** + * Indicates whether the output step has an optional result. Individual conditional attributes + * are not optional; however, rules and subrules can have optional output. + */ + private final boolean isOptional; + + /** True if the condition expression is not trivially true. */ + private final boolean isConditional; + + /** The condition associated with the output. */ + private final CelMutableAst cond; + + /** The output expression for the step. */ + private final CelMutableAst expr; + + private Step( + boolean isOptional, boolean isConditional, CelMutableAst cond, CelMutableAst expr) { + this.isOptional = isOptional; + this.isConditional = isConditional; + this.cond = cond; + this.expr = expr; + } + + private static Step newOptionalStep( + boolean isConditional, CelMutableAst cond, CelMutableAst expr) { + return new Step(/* isOptional= */ true, isConditional, cond, expr); + } + + private static Step newNonOptionalStep( + boolean isConditional, CelMutableAst cond, CelMutableAst expr) { + return new Step(/* isOptional= */ false, isConditional, cond, expr); + } + + private static Step newUnconditionalOptionalStep( + CelMutableAst trueCondition, CelMutableAst expr) { + return new Step(/* isOptional= */ true, /* isConditional= */ false, trueCondition, expr); + } + + private static Step newUnconditionalNonOptionalStep( + CelMutableAst trueCondition, CelMutableAst expr) { + return new Step(/* isOptional= */ false, /* isConditional= */ false, trueCondition, expr); + } } static final class RuleCompositionException extends RuntimeException { @@ -225,4 +345,10 @@ private RuleCompositionException( this.compileException = e; } } + + private RuleComposer(CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) { + this.compiledRule = checkNotNull(compiledRule); + this.variablePrefix = variablePrefix; + this.astMutator = AstMutator.newInstance(iterationLimit); + } } diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java index d4ca76324..35e249407 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java @@ -214,7 +214,30 @@ public void evaluateYamlPolicy_withCanonicalTestData( // Read the policy source String policySource = testData.yamlPolicy.readPolicyYamlContent(); CelPolicy policy = POLICY_PARSER.parse(policySource); - CelAbstractSyntaxTree expectedOutputAst = cel.compile(testData.testCase.getOutput()).getAst(); + Object outputObj = testData.testCase.getOutput(); + String exprToCompile; + if (outputObj instanceof String) { + exprToCompile = (String) outputObj; + } else if (outputObj instanceof Map) { + @SuppressWarnings("unchecked") // Test only + Map outputMap = (Map) outputObj; + if (outputMap.containsKey("value")) { + Object value = outputMap.get("value"); + if (value instanceof String) { + String escapedValue = ((String) value).replace("\"", "\\\""); + exprToCompile = "\"" + escapedValue + "\""; // Quote string literals + } else { + exprToCompile = String.valueOf(value); + } + } else if (outputMap.containsKey("expr")) { + exprToCompile = (String) outputMap.get("expr"); + } else { + throw new IllegalArgumentException("Invalid output format: " + outputObj); + } + } else { + throw new IllegalArgumentException("Invalid output format: " + outputObj); + } + CelAbstractSyntaxTree expectedOutputAst = cel.compile(exprToCompile).getAst(); Object expectedOutput = cel.createProgram(expectedOutputAst).eval(); // Act @@ -266,8 +289,8 @@ public void evaluateYamlPolicy_nestedRuleProducesOptionalOutput() throws Excepti CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); Optional evalResult = (Optional) cel.createProgram(compiledPolicyAst).eval(); - // Result is Optional> - assertThat(evalResult).hasValue(Optional.of(true)); + // Result is Optional containing true + assertThat(evalResult).hasValue(true); } @Test diff --git a/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java b/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java index 18d5ffc69..59647f4d9 100644 --- a/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java +++ b/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java @@ -40,11 +40,11 @@ final class PolicyTestHelper { enum TestYamlPolicy { NESTED_RULE( "nested_rule", - true, + false, "cel.@block([resource.origin, @index0 in [\"us\", \"uk\", \"es\"], {\"banned\": true}]," + " ((@index0 in {\"us\": false, \"ru\": false, \"ir\": false} && !@index1) ?" - + " optional.of(@index2) : optional.none()).or(optional.of(@index1 ? {\"banned\":" - + " false} : @index2)))"), + + " optional.of(@index2) : optional.none()).orValue(@index1 ? {\"banned\":" + + " false} : @index2))"), NESTED_RULE2( "nested_rule2", false, @@ -61,6 +61,22 @@ enum TestYamlPolicy { + " false, \"ru\": false, \"ir\": false} && @index1) ? {\"banned\":" + " \"restricted_region\"} : {\"banned\": \"bad_actor\"}) : (@index1 ?" + " optional.of({\"banned\": \"unconfigured_region\"}) : optional.none()))"), + NESTED_RULE4("nested_rule4", false, "(x > 0) ? true : false"), + NESTED_RULE5( + "nested_rule5", + true, + "cel.@block([optional.of(true), optional.none()], (x > 0) ? ((x > 2) ? @index0 : @index1) :" + + " ((x > 1) ? ((x >= 2) ? @index0 : @index1) : optional.of(false)))"), + NESTED_RULE6( + "nested_rule6", + false, + "cel.@block([optional.of(true), optional.none()], ((x > 2) ? @index0 : @index1).orValue(((x" + + " > 3) ? @index0 : @index1).orValue(false)))"), + NESTED_RULE7( + "nested_rule7", + true, + "cel.@block([optional.of(true), optional.none()], ((x > 2) ? @index0 : @index1).or(((x > 3)" + + " ? @index0 : @index1).or((x > 1) ? optional.of(false) : @index1)))"), REQUIRED_LABELS( "required_labels", true, @@ -198,7 +214,7 @@ public List getTests() { public static final class PolicyTestCase { private String name; private Map input; - private String output; + private Object output; public void setName(String name) { this.name = name; @@ -208,7 +224,7 @@ public void setInput(Map input) { this.input = input; } - public void setOutput(String output) { + public void setOutput(Object output) { this.output = output; } @@ -220,7 +236,7 @@ public Map getInput() { return input; } - public String getOutput() { + public Object getOutput() { return output; } diff --git a/testing/src/test/resources/policy/k8s/tests.yaml b/testing/src/test/resources/policy/k8s/tests.yaml index 8585c5efb..f3e7de790 100644 --- a/testing/src/test/resources/policy/k8s/tests.yaml +++ b/testing/src/test/resources/policy/k8s/tests.yaml @@ -14,18 +14,19 @@ description: K8s admission control tests section: -- name: "invalid" - tests: - - name: "restricted_container" - input: - resource.namespace: - value: "dev.cel" - resource.labels: - value: - environment: "staging" - resource.containers: - value: - - staging.dev.cel.container1 - - staging.dev.cel.container2 - - preprod.dev.cel.container3 - output: "'only staging containers are allowed in namespace dev.cel'" + - name: "invalid" + tests: + - name: "restricted_container" + input: + resource.namespace: + value: "dev.cel" + resource.labels: + value: + environment: "staging" + resource.containers: + value: + - staging.dev.cel.container1 + - staging.dev.cel.container2 + - preprod.dev.cel.container3 + output: + value: "only staging containers are allowed in namespace dev.cel" diff --git a/testing/src/test/resources/policy/limits/tests.yaml b/testing/src/test/resources/policy/limits/tests.yaml index fe6daa61d..88772e075 100644 --- a/testing/src/test/resources/policy/limits/tests.yaml +++ b/testing/src/test/resources/policy/limits/tests.yaml @@ -14,25 +14,29 @@ description: Limits related tests section: -- name: "now_after_hours" - tests: - - name: "7pm" - input: - now: - expr: "timestamp('2024-07-30T00:30:00Z')" - output: "'hello, me'" - - name: "8pm" - input: - now: - expr: "timestamp('2024-07-30T20:30:00Z')" - output: "'goodbye, me!'" - - name: "9pm" - input: - now: - expr: "timestamp('2024-07-30T21:30:00Z')" - output: "'goodbye, me!!'" - - name: "11pm" - input: - now: - expr: "timestamp('2024-07-30T23:30:00Z')" - output: "'goodbye, me!!!'" \ No newline at end of file + - name: "now_after_hours" + tests: + - name: "7pm" + input: + now: + expr: "timestamp('2024-07-30T00:30:00Z')" + output: + value: "hello, me" + - name: "8pm" + input: + now: + expr: "timestamp('2024-07-30T20:30:00Z')" + output: + value: "goodbye, me!" + - name: "9pm" + input: + now: + expr: "timestamp('2024-07-30T21:30:00Z')" + output: + value: "goodbye, me!!" + - name: "11pm" + input: + now: + expr: "timestamp('2024-07-30T23:30:00Z')" + output: + value: "goodbye, me!!!" diff --git a/testing/src/test/resources/policy/nested_rule/tests.yaml b/testing/src/test/resources/policy/nested_rule/tests.yaml index a9807c376..3f9f63437 100644 --- a/testing/src/test/resources/policy/nested_rule/tests.yaml +++ b/testing/src/test/resources/policy/nested_rule/tests.yaml @@ -16,23 +16,26 @@ description: Nested rule conformance tests section: - name: "banned" tests: - - name: "restricted_origin" - input: - resource: - value: - origin: "ir" - output: "{'banned': true}" - - name: "by_default" - input: - resource: - value: - origin: "de" - output: "{'banned': true}" + - name: "restricted_origin" + input: + resource: + value: + origin: "ir" + output: + expr: "{'banned': true}" + - name: "by_default" + input: + resource: + value: + origin: "de" + output: + expr: "{'banned': true}" - name: "permitted" tests: - - name: "valid_origin" - input: - resource: - value: - origin: "uk" - output: "{'banned': false}" + - name: "valid_origin" + input: + resource: + value: + origin: "uk" + output: + expr: "{'banned': false}" diff --git a/testing/src/test/resources/policy/nested_rule2/tests.yaml b/testing/src/test/resources/policy/nested_rule2/tests.yaml index b5fbba745..0e1a9ca69 100644 --- a/testing/src/test/resources/policy/nested_rule2/tests.yaml +++ b/testing/src/test/resources/policy/nested_rule2/tests.yaml @@ -14,35 +14,39 @@ description: Nested rule conformance tests section: -- name: "banned" - tests: - - name: "restricted_origin" - input: - resource: - value: - user: "bad-user" - origin: "ir" - output: "{'banned': 'restricted_region'}" - - name: "by_default" - input: - resource: - value: - user: "bad-user" - origin: "de" - output: "{'banned': 'bad_actor'}" - - name: "unconfigured_region" - input: - resource: - value: - user: "good-user" - origin: "de" - output: "{'banned': 'unconfigured_region'}" -- name: "permitted" - tests: - - name: "valid_origin" - input: - resource: - value: - user: "good-user" - origin: "uk" - output: "{}" \ No newline at end of file + - name: "banned" + tests: + - name: "restricted_origin" + input: + resource: + value: + user: "bad-user" + origin: "ir" + output: + expr: "{'banned': 'restricted_region'}" + - name: "by_default" + input: + resource: + value: + user: "bad-user" + origin: "de" + output: + expr: "{'banned': 'bad_actor'}" + - name: "unconfigured_region" + input: + resource: + value: + user: "good-user" + origin: "de" + output: + expr: "{'banned': 'unconfigured_region'}" + - name: "permitted" + tests: + - name: "valid_origin" + input: + resource: + value: + user: "good-user" + origin: "uk" + output: + expr: "{}" diff --git a/testing/src/test/resources/policy/nested_rule3/tests.yaml b/testing/src/test/resources/policy/nested_rule3/tests.yaml index b10785d0c..9d993c65f 100644 --- a/testing/src/test/resources/policy/nested_rule3/tests.yaml +++ b/testing/src/test/resources/policy/nested_rule3/tests.yaml @@ -14,35 +14,39 @@ description: Nested rule conformance tests section: -- name: "banned" - tests: - - name: "restricted_origin" - input: - resource: - value: - user: "bad-user" - origin: "ir" - output: "{'banned': 'restricted_region'}" - - name: "by_default" - input: - resource: - value: - user: "bad-user" - origin: "de" - output: "{'banned': 'bad_actor'}" - - name: "unconfigured_region" - input: - resource: - value: - user: "good-user" - origin: "de" - output: "{'banned': 'unconfigured_region'}" -- name: "permitted" - tests: - - name: "valid_origin" - input: - resource: - value: - user: "good-user" - origin: "uk" - output: "optional.none()" \ No newline at end of file + - name: "banned" + tests: + - name: "restricted_origin" + input: + resource: + value: + user: "bad-user" + origin: "ir" + output: + expr: "{'banned': 'restricted_region'}" + - name: "by_default" + input: + resource: + value: + user: "bad-user" + origin: "de" + output: + expr: "{'banned': 'bad_actor'}" + - name: "unconfigured_region" + input: + resource: + value: + user: "good-user" + origin: "de" + output: + expr: "{'banned': 'unconfigured_region'}" + - name: "permitted" + tests: + - name: "valid_origin" + input: + resource: + value: + user: "good-user" + origin: "uk" + output: + expr: "optional.none()" diff --git a/testing/src/test/resources/policy/nested_rule4/config.yaml b/testing/src/test/resources/policy/nested_rule4/config.yaml new file mode 100644 index 000000000..5afb8c587 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule4/config.yaml @@ -0,0 +1,19 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "nested_rule4" +variables: + - name: x + type: + type_name: int diff --git a/testing/src/test/resources/policy/nested_rule4/policy.yaml b/testing/src/test/resources/policy/nested_rule4/policy.yaml new file mode 100644 index 000000000..ea53bfb25 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule4/policy.yaml @@ -0,0 +1,24 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: nested_rule4 +rule: + match: + - condition: x > 0 + rule: + match: + - rule: + match: + - output: "true" + - output: "false" diff --git a/testing/src/test/resources/policy/nested_rule4/tests.yaml b/testing/src/test/resources/policy/nested_rule4/tests.yaml new file mode 100644 index 000000000..006eddb88 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule4/tests.yaml @@ -0,0 +1,30 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +description: "Nested rule tests which explore optional vs non-optional returns" +section: + - name: "valid" + tests: + - name: "x=0" + input: + x: + value: 0 + output: + value: false + - name: "x=2" + input: + x: + value: 2 + output: + value: true diff --git a/testing/src/test/resources/policy/nested_rule5/config.yaml b/testing/src/test/resources/policy/nested_rule5/config.yaml new file mode 100644 index 000000000..499450090 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule5/config.yaml @@ -0,0 +1,19 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "nested_rule5" +variables: + - name: x + type: + type_name: int diff --git a/testing/src/test/resources/policy/nested_rule5/policy.yaml b/testing/src/test/resources/policy/nested_rule5/policy.yaml new file mode 100644 index 000000000..e43dce188 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule5/policy.yaml @@ -0,0 +1,30 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: nested_rule5 +rule: + match: + - condition: x > 0 + rule: + match: + - rule: + match: + - condition: "x > 2" + output: "true" + - condition: x > 1 + rule: + match: + - condition: "x >= 2" + output: "true" + - output: "false" diff --git a/testing/src/test/resources/policy/nested_rule5/tests.yaml b/testing/src/test/resources/policy/nested_rule5/tests.yaml new file mode 100644 index 000000000..8cd794051 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule5/tests.yaml @@ -0,0 +1,42 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +description: "Nested rule tests which explore optional vs non-optional returns" +section: + - name: "valid" + tests: + - name: "x=0" + input: + x: + value: 0 + output: + value: false + - name: "x=1" + input: + x: + value: 1 + output: + expr: "optional.none()" + - name: "x=2" + input: + x: + value: 2 + output: + expr: "optional.none()" + - name: "x=3" + input: + x: + value: 3 + output: + value: true diff --git a/testing/src/test/resources/policy/nested_rule6/config.yaml b/testing/src/test/resources/policy/nested_rule6/config.yaml new file mode 100644 index 000000000..a5b1ee16b --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule6/config.yaml @@ -0,0 +1,19 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "nested_rule6" +variables: + - name: x + type: + type_name: int diff --git a/testing/src/test/resources/policy/nested_rule6/policy.yaml b/testing/src/test/resources/policy/nested_rule6/policy.yaml new file mode 100644 index 000000000..a3360e7c1 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule6/policy.yaml @@ -0,0 +1,28 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: nested_rule6 +rule: + match: + - rule: + match: + - rule: + match: + - condition: "x > 2" + output: "true" + - rule: + match: + - condition: "x > 3" + output: "true" + - output: "false" diff --git a/testing/src/test/resources/policy/nested_rule6/tests.yaml b/testing/src/test/resources/policy/nested_rule6/tests.yaml new file mode 100644 index 000000000..fef586df0 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule6/tests.yaml @@ -0,0 +1,24 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +description: "Nested rule tests which explore optional vs non-optional returns" +section: + - name: "valid" + tests: + - name: "x=0" + input: + x: + value: 0 + output: + value: false diff --git a/testing/src/test/resources/policy/nested_rule7/config.yaml b/testing/src/test/resources/policy/nested_rule7/config.yaml new file mode 100644 index 000000000..74d4d8c2d --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule7/config.yaml @@ -0,0 +1,19 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "nested_rule7" +variables: + - name: x + type: + type_name: int diff --git a/testing/src/test/resources/policy/nested_rule7/policy.yaml b/testing/src/test/resources/policy/nested_rule7/policy.yaml new file mode 100644 index 000000000..fcacd017e --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule7/policy.yaml @@ -0,0 +1,29 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: nested_rule7 +rule: + match: + - rule: + match: + - rule: + match: + - condition: "x > 2" + output: "true" + - rule: + match: + - condition: "x > 3" + output: "true" + - condition: "x > 1" + output: "false" diff --git a/testing/src/test/resources/policy/nested_rule7/tests.yaml b/testing/src/test/resources/policy/nested_rule7/tests.yaml new file mode 100644 index 000000000..ec2896878 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule7/tests.yaml @@ -0,0 +1,42 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +description: "Nested rule tests which explore optional vs non-optional returns" +section: + - name: "valid" + tests: + - name: "x=1" + input: + x: + value: 1 + output: + expr: "optional.none()" + - name: "x=2" + input: + x: + value: 2 + output: + value: false + - name: "x=3" + input: + x: + value: 3 + output: + value: true + - name: "x=4" + input: + x: + value: 4 + output: + value: true diff --git a/testing/src/test/resources/policy/pb/tests.yaml b/testing/src/test/resources/policy/pb/tests.yaml index 82dd6b11b..71cd56b57 100644 --- a/testing/src/test/resources/policy/pb/tests.yaml +++ b/testing/src/test/resources/policy/pb/tests.yaml @@ -14,20 +14,21 @@ description: "Protobuf input tests" section: -- name: "valid" - tests: - - name: "good spec" - input: - spec: - expr: > - TestAllTypes{single_int32: 10} - output: "optional.none()" -- name: "invalid" - tests: - - name: "bad spec" - input: - spec: - expr: > - TestAllTypes{single_int32: 11} - output: > - "invalid spec, got single_int32=11, wanted <= 10" + - name: "valid" + tests: + - name: "good spec" + input: + spec: + expr: > + TestAllTypes{single_int32: 10} + output: + expr: "optional.none()" + - name: "invalid" + tests: + - name: "bad spec" + input: + spec: + expr: > + TestAllTypes{single_int32: 11} + output: + value: "invalid spec, got single_int32=11, wanted <= 10" diff --git a/testing/src/test/resources/policy/required_labels/tests.yaml b/testing/src/test/resources/policy/required_labels/tests.yaml index 67681ef46..4296c6914 100644 --- a/testing/src/test/resources/policy/required_labels/tests.yaml +++ b/testing/src/test/resources/policy/required_labels/tests.yaml @@ -16,64 +16,65 @@ description: "Required labels conformance tests" section: - name: "valid" tests: - - name: "matching" - input: - spec: - value: - labels: - env: prod - experiment: "group b" - resource: - value: - labels: - env: prod - experiment: "group b" - release: "v0.1.0" - output: "optional.none()" + - name: "matching" + input: + spec: + value: + labels: + env: prod + experiment: "group b" + resource: + value: + labels: + env: prod + experiment: "group b" + release: "v0.1.0" + output: + expr: "optional.none()" - name: "missing" tests: - - name: "env" - input: - spec: - value: - labels: - env: prod - experiment: "group b" - resource: - value: - labels: - experiment: "group b" - release: "v0.1.0" - output: > - "missing one or more required labels: [\"env\"]" - - name: "experiment" - input: - spec: - value: - labels: - env: prod - experiment: "group b" - resource: - value: - labels: - env: staging - release: "v0.1.0" - output: > - "missing one or more required labels: [\"experiment\"]" + - name: "env" + input: + spec: + value: + labels: + env: prod + experiment: "group b" + resource: + value: + labels: + experiment: "group b" + release: "v0.1.0" + output: + value: "missing one or more required labels: [\"env\"]" + - name: "experiment" + input: + spec: + value: + labels: + env: prod + experiment: "group b" + resource: + value: + labels: + env: staging + release: "v0.1.0" + output: + value: "missing one or more required labels: [\"experiment\"]" - name: "invalid" tests: - - name: "env" - input: - spec: - value: - labels: - env: prod - experiment: "group b" - resource: - value: - labels: - env: staging - experiment: "group b" - release: "v0.1.0" - output: > - "invalid values provided on one or more labels: [\"env\"]" + - name: "env" + input: + spec: + value: + labels: + env: prod + experiment: "group b" + resource: + value: + labels: + env: staging + experiment: "group b" + release: "v0.1.0" + output: + value: "invalid values provided on one or more labels: [\"env\"]" diff --git a/testing/src/test/resources/policy/restricted_destinations/tests.yaml b/testing/src/test/resources/policy/restricted_destinations/tests.yaml index c0feeb202..f7ae36550 100644 --- a/testing/src/test/resources/policy/restricted_destinations/tests.yaml +++ b/testing/src/test/resources/policy/restricted_destinations/tests.yaml @@ -16,103 +16,107 @@ description: Restricted destinations conformance tests. section: - name: "valid" tests: - - name: "ip_allowed" - input: - "spec.origin": - value: "us" - "spec.restricted_destinations": - value: - - "cu" - - "ir" - - "kp" - - "sd" - - "sy" - "destination.ip": - value: "10.0.0.1" - "origin.ip": - value: "10.0.0.1" - request: - value: - auth: - claims: {} - resource: - value: - name: "/company/acme/secrets/doomsday-device" - labels: - location: "us" - output: "false" # false means unrestricted - - name: "nationality_allowed" - input: - "spec.origin": - value: "us" - "spec.restricted_destinations": - value: - - "cu" - - "ir" - - "kp" - - "sd" - - "sy" - "destination.ip": - value: "10.0.0.1" - request: - value: - auth: - claims: - nationality: "us" - resource: - value: - name: "/company/acme/secrets/doomsday-device" - labels: - location: "us" - output: "false" + - name: "ip_allowed" + input: + spec.origin: + value: "us" + spec.restricted_destinations: + value: + - "cu" + - "ir" + - "kp" + - "sd" + - "sy" + destination.ip: + value: "10.0.0.1" + origin.ip: + value: "10.0.0.1" + request: + value: + auth: + claims: {} + resource: + value: + name: "/company/acme/secrets/doomsday-device" + labels: + location: "us" + output: + value: false # false means unrestricted + - name: "nationality_allowed" + input: + spec.origin: + value: "us" + spec.restricted_destinations: + value: + - "cu" + - "ir" + - "kp" + - "sd" + - "sy" + destination.ip: + value: "10.0.0.1" + request: + value: + auth: + claims: + nationality: "us" + resource: + value: + name: "/company/acme/secrets/doomsday-device" + labels: + location: "us" + output: + value: false - name: "invalid" tests: - - name: "destination_ip_prohibited" - input: - "spec.origin": - value: "us" - "spec.restricted_destinations": - value: - - "cu" - - "ir" - - "kp" - - "sd" - - "sy" - "destination.ip": - value: "123.123.123.123" - "origin.ip": - value: "10.0.0.1" - request: - value: - auth: - claims: {} - resource: - value: - name: "/company/acme/secrets/doomsday-device" - labels: - location: "us" - output: "true" # true means restricted - - name: "resource_nationality_prohibited" - input: - "spec.origin": - value: "us" - "spec.restricted_destinations": - value: - - "cu" - - "ir" - - "kp" - - "sd" - - "sy" - "destination.ip": - value: "10.0.0.1" - request: - value: - auth: - claims: - nationality: "us" - resource: - value: - name: "/company/acme/secrets/doomsday-device" - labels: - location: "cu" - output: "true" + - name: "destination_ip_prohibited" + input: + spec.origin: + value: "us" + spec.restricted_destinations: + value: + - "cu" + - "ir" + - "kp" + - "sd" + - "sy" + destination.ip: + value: "123.123.123.123" + origin.ip: + value: "10.0.0.1" + request: + value: + auth: + claims: {} + resource: + value: + name: "/company/acme/secrets/doomsday-device" + labels: + location: "us" + output: + value: true # true means restricted + - name: "resource_nationality_prohibited" + input: + spec.origin: + value: "us" + spec.restricted_destinations: + value: + - "cu" + - "ir" + - "kp" + - "sd" + - "sy" + destination.ip: + value: "10.0.0.1" + request: + value: + auth: + claims: + nationality: "us" + resource: + value: + name: "/company/acme/secrets/doomsday-device" + labels: + location: "cu" + output: + value: true