Skip to content

Commit 89ea79c

Browse files
l46kokcopybara-github
authored andcommitted
Add unreachable checks in policies
PiperOrigin-RevId: 663120461
1 parent 80d29a9 commit 89ea79c

9 files changed

Lines changed: 169 additions & 22 deletions

File tree

policy/src/main/java/dev/cel/policy/CelCompiledRule.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@
3030
*/
3131
@AutoValue
3232
public abstract class CelCompiledRule {
33-
public abstract Optional<ValueString> id();
33+
34+
/** Source metadata identifier associated with the compiled rule. */
35+
public abstract long sourceId();
36+
37+
public abstract Optional<ValueString> ruleId();
3438

3539
public abstract ImmutableList<CelCompiledVariable> variables();
3640

@@ -50,7 +54,7 @@ public boolean hasOptionalOutput() {
5054
return true;
5155
}
5256

53-
if (match.isConditionLiteral()) {
57+
if (match.isConditionTriviallyTrue()) {
5458
return false;
5559
}
5660

@@ -83,11 +87,14 @@ static CelCompiledVariable create(
8387
/** A compiled Match. */
8488
@AutoValue
8589
public abstract static class CelCompiledMatch {
90+
/** Source metadata identifier associated with the compiled match. */
91+
public abstract long sourceId();
92+
8693
public abstract CelAbstractSyntaxTree condition();
8794

8895
public abstract Result result();
8996

90-
public boolean isConditionLiteral() {
97+
public boolean isConditionTriviallyTrue() {
9198
CelExpr celExpr = condition().getExpr();
9299
return celExpr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE)
93100
&& celExpr.constant().booleanValue();
@@ -136,16 +143,17 @@ public static OutputValue create(long id, CelAbstractSyntaxTree ast) {
136143
}
137144

138145
static CelCompiledMatch create(
139-
CelAbstractSyntaxTree condition, CelCompiledMatch.Result result) {
140-
return new AutoValue_CelCompiledRule_CelCompiledMatch(condition, result);
146+
long sourceId, CelAbstractSyntaxTree condition, CelCompiledMatch.Result result) {
147+
return new AutoValue_CelCompiledRule_CelCompiledMatch(sourceId, condition, result);
141148
}
142149
}
143150

144151
static CelCompiledRule create(
145-
Optional<ValueString> id,
152+
long sourceId,
153+
Optional<ValueString> ruleId,
146154
ImmutableList<CelCompiledVariable> variables,
147155
ImmutableList<CelCompiledMatch> matches,
148156
Cel cel) {
149-
return new AutoValue_CelCompiledRule(id, variables, matches, cel);
157+
return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel);
150158
}
151159
}

policy/src/main/java/dev/cel/policy/CelPolicy.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public abstract class CelPolicy {
4545
public static Builder newBuilder() {
4646
return new AutoValue_CelPolicy.Builder()
4747
.setName(ValueString.of(0, ""))
48-
.setRule(Rule.newBuilder().build())
48+
.setRule(Rule.newBuilder(0).build())
4949
.setMetadata(ImmutableMap.of());
5050
}
5151

@@ -86,8 +86,9 @@ public Builder putMetadata(Map<String, Object> map) {
8686
*/
8787
@AutoValue
8888
public abstract static class Rule {
89+
public abstract long id();
8990

90-
public abstract Optional<ValueString> id();
91+
public abstract Optional<ValueString> ruleId();
9192

9293
public abstract Optional<ValueString> description();
9394

@@ -96,8 +97,9 @@ public abstract static class Rule {
9697
public abstract ImmutableSet<Match> matches();
9798

9899
/** Builder for {@link Rule}. */
99-
public static Builder newBuilder() {
100+
public static Builder newBuilder(long id) {
100101
return new AutoValue_CelPolicy_Rule.Builder()
102+
.setId(id)
101103
.setVariables(ImmutableSet.of())
102104
.setMatches(ImmutableSet.of());
103105
}
@@ -106,7 +108,7 @@ public static Builder newBuilder() {
106108
@AutoValue.Builder
107109
public abstract static class Builder {
108110

109-
public abstract Rule.Builder setId(ValueString id);
111+
public abstract Rule.Builder setRuleId(ValueString id);
110112

111113
public abstract Rule.Builder setDescription(ValueString description);
112114

@@ -118,6 +120,8 @@ public abstract static class Builder {
118120

119121
abstract ImmutableSet.Builder<Match> matchesBuilder();
120122

123+
abstract Builder setId(long value);
124+
121125
@CanIgnoreReturnValue
122126
public Builder addVariables(Variable... variables) {
123127
return addVariables(Arrays.asList(variables));
@@ -159,6 +163,8 @@ public abstract static class Match {
159163

160164
public abstract Result result();
161165

166+
public abstract long id();
167+
162168
/** Explanation returns the explanation expression, or empty expression if output is not set. */
163169
public abstract Optional<ValueString> explanation();
164170

@@ -189,13 +195,16 @@ public enum Kind {
189195
/** Builder for {@link Match}. */
190196
@AutoValue.Builder
191197
public abstract static class Builder implements RequiredFieldsChecker {
198+
public abstract Builder setId(long value);
192199

193200
public abstract Builder setCondition(ValueString condition);
194201

195202
public abstract Builder setResult(Result result);
196203

197204
public abstract Builder setExplanation(ValueString explanation);
198205

206+
abstract Optional<Long> id();
207+
199208
abstract Optional<Result> result();
200209

201210
abstract Optional<ValueString> explanation();
@@ -209,8 +218,8 @@ public ImmutableList<RequiredField> requiredFields() {
209218
}
210219

211220
/** Creates a new builder to construct a {@link Match} instance. */
212-
public static Builder newBuilder() {
213-
return new AutoValue_CelPolicy_Match.Builder();
221+
public static Builder newBuilder(long id) {
222+
return new AutoValue_CelPolicy_Match.Builder().setId(id);
214223
}
215224
}
216225

policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import dev.cel.optimizer.CelOptimizerFactory;
3636
import dev.cel.policy.CelCompiledRule.CelCompiledMatch;
3737
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
38+
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result.Kind;
3839
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
3940
import dev.cel.policy.CelPolicy.Match;
4041
import dev.cel.policy.CelPolicy.Variable;
@@ -167,10 +168,39 @@ private CelCompiledRule compileRuleImpl(
167168
throw new IllegalArgumentException("Unexpected kind: " + match.result().kind());
168169
}
169170

170-
matchBuilder.add(CelCompiledMatch.create(conditionAst, matchResult));
171+
matchBuilder.add(CelCompiledMatch.create(match.id(), conditionAst, matchResult));
171172
}
172173

173-
return CelCompiledRule.create(rule.id(), variableBuilder.build(), matchBuilder.build(), cel);
174+
CelCompiledRule compiledRule =
175+
CelCompiledRule.create(
176+
rule.id(), rule.ruleId(), variableBuilder.build(), matchBuilder.build(), cel);
177+
178+
// Validate that all branches in the policy are reachable
179+
checkUnreachableCode(compiledRule, compilerContext);
180+
181+
return compiledRule;
182+
}
183+
184+
private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext compilerContext) {
185+
boolean ruleHasOptional = compiledRule.hasOptionalOutput();
186+
ImmutableList<CelCompiledMatch> compiledMatches = compiledRule.matches();
187+
int matchCount = compiledMatches.size();
188+
for (int i = matchCount - 1; i >= 0; i--) {
189+
CelCompiledMatch compiledMatch = compiledMatches.get(i);
190+
boolean isTriviallyTrue = compiledMatch.isConditionTriviallyTrue();
191+
192+
if (isTriviallyTrue && !ruleHasOptional && i != matchCount - 1) {
193+
if (compiledMatch.result().kind().equals(Kind.OUTPUT)) {
194+
compilerContext.addIssue(
195+
compiledMatch.sourceId(),
196+
CelIssue.formatError(1, 0, "Match creates unreachable outputs"));
197+
} else {
198+
compilerContext.addIssue(
199+
compiledMatch.result().rule().sourceId(),
200+
CelIssue.formatError(1, 0, "Rule creates unreachable outputs"));
201+
}
202+
}
203+
}
174204
}
175205

176206
private static CelAbstractSyntaxTree newErrorAst() {

policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ final class CelPolicyYamlParser implements CelPolicyParser {
4040
// Sentinel values for parsing errors
4141
private static final ValueString ERROR_VALUE = ValueString.newBuilder().setValue(ERROR).build();
4242
private static final Match ERROR_MATCH =
43-
Match.newBuilder().setCondition(ERROR_VALUE).setResult(Result.ofOutput(ERROR_VALUE)).build();
43+
Match.newBuilder(0).setCondition(ERROR_VALUE).setResult(Result.ofOutput(ERROR_VALUE)).build();
4444
private static final Variable ERROR_VARIABLE =
4545
Variable.newBuilder().setExpression(ERROR_VALUE).setName(ERROR_VALUE).build();
4646

@@ -122,7 +122,7 @@ public CelPolicy parsePolicy(PolicyParserContext<Node> ctx, Node node) {
122122
public CelPolicy.Rule parseRule(
123123
PolicyParserContext<Node> ctx, CelPolicy.Builder policyBuilder, Node node) {
124124
long valueId = ctx.collectMetadata(node);
125-
CelPolicy.Rule.Builder ruleBuilder = CelPolicy.Rule.newBuilder();
125+
CelPolicy.Rule.Builder ruleBuilder = CelPolicy.Rule.newBuilder(valueId);
126126
if (!assertYamlType(ctx, valueId, node, YamlNodeType.MAP)) {
127127
return ruleBuilder.build();
128128
}
@@ -137,7 +137,7 @@ public CelPolicy.Rule parseRule(
137137
Node value = nodeTuple.getValueNode();
138138
switch (fieldName) {
139139
case "id":
140-
ruleBuilder.setId(ctx.newValueString(value));
140+
ruleBuilder.setRuleId(ctx.newValueString(value));
141141
break;
142142
case "description":
143143
ruleBuilder.setDescription(ctx.newValueString(value));
@@ -181,7 +181,7 @@ public CelPolicy.Match parseMatch(
181181
}
182182
MappingNode matchNode = (MappingNode) node;
183183
CelPolicy.Match.Builder matchBuilder =
184-
CelPolicy.Match.newBuilder().setCondition(ValueString.of(ctx.nextId(), "true"));
184+
CelPolicy.Match.newBuilder(nodeId).setCondition(ValueString.of(ctx.nextId(), "true"));
185185
for (NodeTuple nodeTuple : matchNode.getValue()) {
186186
Node key = nodeTuple.getKeyNode();
187187
long tagId = ctx.collectMetadata(key);

policy/src/main/java/dev/cel/policy/RuleComposer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ private RuleOptimizationResult optimizeRule(Cel cel, CelCompiledRule compiledRul
7777
// If the condition is trivially true, none of the matches in the rule causes the result
7878
// to become optional, and the rule is not the last match, then this will introduce
7979
// unreachable outputs or rules.
80-
boolean isTriviallyTrue = match.isConditionLiteral();
80+
boolean isTriviallyTrue = match.isConditionTriviallyTrue();
8181

8282
switch (match.result().kind()) {
8383
// For the match's output, determine whether the output should be wrapped
@@ -144,7 +144,7 @@ private RuleOptimizationResult optimizeRule(Cel cel, CelCompiledRule compiledRul
144144
matchAst,
145145
String.format(
146146
"failed composing the subrule '%s' due to conflicting output types.",
147-
matchNestedRule.id().map(ValueString::value).orElse("")),
147+
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
148148
lastOutputId);
149149
break;
150150
}

policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,8 @@ private enum MultilineErrorTest {
308308
private enum TestErrorYamlPolicy {
309309
COMPILE_ERRORS("compile_errors"),
310310
COMPOSE_ERRORS_CONFLICTING_OUTPUT("compose_errors_conflicting_output"),
311-
COMPOSE_ERRORS_CONFLICTING_SUBRULE("compose_errors_conflicting_subrule");
311+
COMPOSE_ERRORS_CONFLICTING_SUBRULE("compose_errors_conflicting_subrule"),
312+
ERRORS_UNREACHABLE("errors_unreachable");
312313

313314
private final String name;
314315
private final String policyFilePath;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Copyright 2024 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
name: "errors_unreachable"
16+
extensions:
17+
- name: "sets"
18+
- name: "strings"
19+
version: "latest"
20+
variables:
21+
- name: "destination.ip"
22+
type:
23+
type_name: "string"
24+
- name: "origin.ip"
25+
type:
26+
type_name: "string"
27+
- name: "spec.restricted_destinations"
28+
type:
29+
type_name: "list"
30+
params:
31+
- type_name: "string"
32+
- name: "spec.origin"
33+
type:
34+
type_name: "string"
35+
- name: "request"
36+
type:
37+
type_name: "map"
38+
params:
39+
- type_name: "string"
40+
- type_name: "dyn"
41+
- name: "resource"
42+
type:
43+
type_name: "map"
44+
params:
45+
- type_name: "string"
46+
- type_name: "dyn"
47+
functions:
48+
- name: "locationCode"
49+
overloads:
50+
- id: "locationCode_string"
51+
args:
52+
- type_name: "string"
53+
return:
54+
type_name: "string"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
ERROR: errors_unreachable/policy.yaml:36:9: Match creates unreachable outputs
2+
| - output: |
3+
| ........^
4+
ERROR: errors_unreachable/policy.yaml:28:7: Rule creates unreachable outputs
5+
| match:
6+
| ......^
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright 2024 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
name: "errors_unreachable"
16+
rule:
17+
variables:
18+
- name: want
19+
expression: request.labels
20+
- name: missing
21+
expression: variables.want.filter(l, !(l in resource.labels))
22+
- name: invalid
23+
expression: >
24+
resource.labels.filter(l,
25+
l in variables.want && variables.want[l] != resource.labels[l])
26+
match:
27+
- rule:
28+
match:
29+
- output: "''"
30+
- condition: variables.missing.size() > 0
31+
output: |
32+
"missing one or more required labels: [\"" + variables.missing.join(',') + "\"]"
33+
- condition: variables.invalid.size() > 0
34+
rule:
35+
match:
36+
- output: |
37+
"invalid values provided on one or more labels: [\"" + variables.invalid.join(',') + "\"]"
38+
- condition: "false"
39+
output: "'unreachable'"

0 commit comments

Comments
 (0)