Skip to content

Commit cf22d2e

Browse files
authored
feat(auto-rules): JavaScript Rule Match Expressions (#544)
* Replace Rule.targetAlias with Rule.matchExpression Use Nashorn to evaluate Rule.matchExpressions against ServiceRefs, allowing Rule definitions to contain JavaScript snippets for testing whether the Rule applies to a given ServicRef * Validate Rule.matchExpression using Nashorn Parser Disallow matchExpressions from containing abusable JavaScript constructs like function invocations, loops, try/catch, etc. * Test fixes * Better error handling and testing serialization * Add event specifically for bindings creation * Remove gson serialization of bindings * Re-add BindingsCreationEvent * Add unit tests for invalid match expressions * Refactoring * Rename exception * Handle IOException in deserialization * Correct failing tests after rebase * Add test for illegal match expressions * Remove redundant check * Correct mock matchExpressions in tests * Provide JS matcher with String alias, not Optional * Add RuleMatcherTest * Minor refactor * Add MatchExpressionValidatorTest * Remove unnecessary StringReader and IOException * Update doc * Add version quick nav list * Extract MatchExpressionTreeVisitor class * Correct test matchExpression * Throw checked exception for match expression validation failure * Remove redundant local variable * Change itest targetAlias to matchExpression
1 parent 2a03522 commit cf22d2e

23 files changed

+1135
-125
lines changed

HTTP_API.md

+17-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# HTTP API
22

3+
* [V1](#V1-API)
4+
* [V2](#V2-API)
5+
36
## V1 API
47

58
### Quick Reference
@@ -1390,15 +1393,22 @@ The handler-specific descriptions below describe how each handler populates the
13901393
`POST /api/v2/rules`
13911394
13921395
The request may be an HTTP form or a JSON document. In either case, the
1393-
attributes `"name"`, `"targetAlias"`, and `"eventSpecifier"` must be
1396+
attributes `"name"`, `"matchExpression"`, and `"eventSpecifier"` must be
13941397
provided.
13951398
13961399
`"name"`: the name of this rule definition. This must be unique. This name
13971400
will also be used to generate the name of the associated recordings.
13981401
1399-
`"targetAlias"`: targets with an exactly matching `alias` will match this
1400-
rule definition, activating this rule for the target and causing the defined
1401-
recording to be started on the target.
1402+
`"matchExpression"`: a string expression used to determine which target JVMs
1403+
this rule will apply to. The expression has a variable named `target` in
1404+
scope, which is of type
1405+
[`ServiceRef`](src/main/java/io/cryostat/platform/ServiceRef.java).
1406+
Properties can be accessed using `.` separators, and the operators `==`,
1407+
`!=`, `||`, and `&&` are accepted, with their usual meanings. An example of
1408+
such an expression is:
1409+
`(target.alias == 'io.cryostat.Cryostat' || target.annotations.cryostat.JAVA_MAIN == 'io.cryostat.Cryostat') && target.annotations.cryostat.PORT != 9091`.
1410+
The simple expression `true` may also be used to create a rule which applies
1411+
to any and all discovered targets.
14021412
14031413
`"eventSpecifier"`: a string of the form `template=Foo,type=TYPE`. This
14041414
defines the event template that will be used for creating new recordings in
@@ -1449,7 +1459,7 @@ The handler-specific descriptions below describe how each handler populates the
14491459
14501460
##### example
14511461
```
1452-
$ curl -X POST -F name="Test Rule" -F description="This is a rule for testing" -F targetAlias="io.cryostat.Cryostat" -F eventSpecifier="template=Continuous,type=TARGET" http://0.0.0.0:8181/api/v2/rules
1462+
$ curl -X POST -F name="Test Rule" -F description="This is a rule for testing" -F matchExpression="target.alias == 'io.cryostat.Cryostat'" -F eventSpecifier="template=Continuous,type=TARGET" http://0.0.0.0:8181/api/v2/rules
14531463
< HTTP/1.1 201 Created
14541464
< location: /api/v2/rules/Test_Rule
14551465
< content-length: 79
@@ -1504,7 +1514,7 @@ The handler-specific descriptions below describe how each handler populates the
15041514
##### example
15051515
```
15061516
$ curl http://0.0.0.0:8181/api/v2/rules/Test_Rule
1507-
{"meta":{"type":"application/json","status":"OK"},"data":{"result":{"name":"Test_Rule","description":"This is a rule for testing","targetAlias":"io.cryostat.Cryostat","eventSpecifier":"template=Continuous,type=TARGET","archivalPeriodSeconds":30,"preservedArchives":1,"maxAgeSeconds":30,"maxSizeBytes":-1}}}
1517+
{"meta":{"type":"application/json","status":"OK"},"data":{"result":{"name":"Test_Rule","description":"This is a rule for testing","matchExpression":"target.alias=='io.cryostat.Cryostat'","eventSpecifier":"template=Continuous,type=TARGET","archivalPeriodSeconds":30,"preservedArchives":1,"maxAgeSeconds":30,"maxSizeBytes":-1}}}
15081518
```
15091519
15101520
* #### `RulesGetHandler`
@@ -1527,8 +1537,7 @@ The handler-specific descriptions below describe how each handler populates the
15271537
##### example
15281538
```
15291539
$ curl http://0.0.0.0:8181/api/v2/rules
1530-
{"meta":{"type":"application/json","status":"OK"},"data":{"result":[{"name":"Test_Rule","description":"This is a rule for testing","targetAlias":"io.cryostat.Cryostat","eventSpecifier":"template=Continuous,type=TARGET","archivalPeriodSeconds":30,"preservedArchives":1,"maxAgeSeconds":30,"maxSizeBytes":-1}]}}
1531-
```
1540+
{"meta":{"type":"application/json","status":"OK"},"data":{"result":[{"name":"Test_Rule","description":"This is a rule for testing","matchExpression":"target.alias=='io.cryostat.Cryostat'","eventSpecifier":"template=Continuous,type=TARGET","archivalPeriodSeconds":30,"preservedArchives":1,"maxAgeSeconds":30,"maxSizeBytes":-1}]}} ```
15321541
15331542
### Stored Target Credentials
15341543

src/main/java/io/cryostat/net/web/http/api/v2/RulesPostHandler.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import io.cryostat.net.AuthManager;
4646
import io.cryostat.net.web.http.HttpMimeType;
4747
import io.cryostat.net.web.http.api.ApiVersion;
48+
import io.cryostat.rules.MatchExpressionValidationException;
4849
import io.cryostat.rules.Rule;
4950
import io.cryostat.rules.RuleException;
5051
import io.cryostat.rules.RuleRegistry;
@@ -121,7 +122,7 @@ public IntermediateResponse<String> handle(RequestParameters params) throws ApiE
121122
try {
122123
Rule.Builder builder = Rule.Builder.from(params.getFormAttributes());
123124
rule = builder.build();
124-
} catch (IllegalArgumentException iae) {
125+
} catch (MatchExpressionValidationException | IllegalArgumentException iae) {
125126
throw new ApiException(400, iae);
126127
}
127128
break;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright The Cryostat Authors
3+
*
4+
* The Universal Permissive License (UPL), Version 1.0
5+
*
6+
* Subject to the condition set forth below, permission is hereby granted to any
7+
* person obtaining a copy of this software, associated documentation and/or data
8+
* (collectively the "Software"), free of charge and under any and all copyright
9+
* rights in the Software, and any and all patent rights owned or freely
10+
* licensable by each licensor hereunder covering either (i) the unmodified
11+
* Software as contributed to or provided by such licensor, or (ii) the Larger
12+
* Works (as defined below), to deal in both
13+
*
14+
* (a) the Software, and
15+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
16+
* one is included with the Software (each a "Larger Work" to which the Software
17+
* is contributed by such licensors),
18+
*
19+
* without restriction, including without limitation the rights to copy, create
20+
* derivative works of, display, perform, and distribute the Software and make,
21+
* use, sell, offer for sale, import, export, have made, and have sold the
22+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
23+
* either these or other terms.
24+
*
25+
* This license is subject to the following condition:
26+
* The above copyright notice and either this complete permission notice or at
27+
* a minimum a reference to the UPL must be included in all copies or
28+
* substantial portions of the Software.
29+
*
30+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
31+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
32+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
33+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
34+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
35+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
36+
* SOFTWARE.
37+
*/
38+
package io.cryostat.rules;
39+
40+
import jdk.nashorn.api.tree.Tree;
41+
42+
@SuppressWarnings("serial")
43+
class IllegalMatchExpressionException extends RuntimeException {
44+
IllegalMatchExpressionException() {
45+
this("matchExpression parsing failed");
46+
}
47+
48+
IllegalMatchExpressionException(String reason) {
49+
super(reason);
50+
}
51+
52+
IllegalMatchExpressionException(Tree node, String matchExpression) {
53+
super(
54+
String.format(
55+
"matchExpression rejected, illegal %s at [%d, %d]: %s",
56+
node.getKind(),
57+
node.getStartPosition(),
58+
node.getEndPosition(),
59+
matchExpression.substring(
60+
(int) node.getStartPosition(), (int) node.getEndPosition())));
61+
}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/*
2+
* Copyright The Cryostat Authors
3+
*
4+
* The Universal Permissive License (UPL), Version 1.0
5+
*
6+
* Subject to the condition set forth below, permission is hereby granted to any
7+
* person obtaining a copy of this software, associated documentation and/or data
8+
* (collectively the "Software"), free of charge and under any and all copyright
9+
* rights in the Software, and any and all patent rights owned or freely
10+
* licensable by each licensor hereunder covering either (i) the unmodified
11+
* Software as contributed to or provided by such licensor, or (ii) the Larger
12+
* Works (as defined below), to deal in both
13+
*
14+
* (a) the Software, and
15+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
16+
* one is included with the Software (each a "Larger Work" to which the Software
17+
* is contributed by such licensors),
18+
*
19+
* without restriction, including without limitation the rights to copy, create
20+
* derivative works of, display, perform, and distribute the Software and make,
21+
* use, sell, offer for sale, import, export, have made, and have sold the
22+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
23+
* either these or other terms.
24+
*
25+
* This license is subject to the following condition:
26+
* The above copyright notice and either this complete permission notice or at
27+
* a minimum a reference to the UPL must be included in all copies or
28+
* substantial portions of the Software.
29+
*
30+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
31+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
32+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
33+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
34+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
35+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
36+
* SOFTWARE.
37+
*/
38+
package io.cryostat.rules;
39+
40+
import jdk.nashorn.api.tree.BreakTree;
41+
import jdk.nashorn.api.tree.CaseTree;
42+
import jdk.nashorn.api.tree.CatchTree;
43+
import jdk.nashorn.api.tree.ClassDeclarationTree;
44+
import jdk.nashorn.api.tree.ClassExpressionTree;
45+
import jdk.nashorn.api.tree.ContinueTree;
46+
import jdk.nashorn.api.tree.DebuggerTree;
47+
import jdk.nashorn.api.tree.DoWhileLoopTree;
48+
import jdk.nashorn.api.tree.ErroneousTree;
49+
import jdk.nashorn.api.tree.ExportEntryTree;
50+
import jdk.nashorn.api.tree.ForInLoopTree;
51+
import jdk.nashorn.api.tree.ForLoopTree;
52+
import jdk.nashorn.api.tree.ForOfLoopTree;
53+
import jdk.nashorn.api.tree.FunctionCallTree;
54+
import jdk.nashorn.api.tree.FunctionDeclarationTree;
55+
import jdk.nashorn.api.tree.FunctionExpressionTree;
56+
import jdk.nashorn.api.tree.ImportEntryTree;
57+
import jdk.nashorn.api.tree.InstanceOfTree;
58+
import jdk.nashorn.api.tree.LabeledStatementTree;
59+
import jdk.nashorn.api.tree.ModuleTree;
60+
import jdk.nashorn.api.tree.NewTree;
61+
import jdk.nashorn.api.tree.RegExpLiteralTree;
62+
import jdk.nashorn.api.tree.ReturnTree;
63+
import jdk.nashorn.api.tree.SimpleTreeVisitorES5_1;
64+
import jdk.nashorn.api.tree.SpreadTree;
65+
import jdk.nashorn.api.tree.ThrowTree;
66+
import jdk.nashorn.api.tree.Tree;
67+
import jdk.nashorn.api.tree.TryTree;
68+
import jdk.nashorn.api.tree.UnaryTree;
69+
import jdk.nashorn.api.tree.WhileLoopTree;
70+
import jdk.nashorn.api.tree.WithTree;
71+
import jdk.nashorn.api.tree.YieldTree;
72+
73+
class MatchExpressionTreeVisitor extends SimpleTreeVisitorES5_1<Void, String> {
74+
private Void fail(Tree node, String matchExpression) {
75+
throw new IllegalMatchExpressionException(node, matchExpression);
76+
}
77+
78+
@Override
79+
public Void visitBreak(BreakTree node, String matchExpression) {
80+
return fail(node, matchExpression);
81+
}
82+
83+
@Override
84+
public Void visitCase(CaseTree node, String matchExpression) {
85+
return fail(node, matchExpression);
86+
}
87+
88+
@Override
89+
public Void visitCatch(CatchTree node, String matchExpression) {
90+
return fail(node, matchExpression);
91+
}
92+
93+
@Override
94+
public Void visitClassDeclaration(ClassDeclarationTree node, String matchExpression) {
95+
return fail(node, matchExpression);
96+
}
97+
98+
@Override
99+
public Void visitClassExpression(ClassExpressionTree node, String matchExpression) {
100+
return fail(node, matchExpression);
101+
}
102+
103+
@Override
104+
public Void visitDoWhileLoop(DoWhileLoopTree node, String matchExpression) {
105+
return fail(node, matchExpression);
106+
}
107+
108+
@Override
109+
public Void visitErroneous(ErroneousTree node, String matchExpression) {
110+
return fail(node, matchExpression);
111+
}
112+
113+
@Override
114+
public Void visitExportEntry(ExportEntryTree node, String matchExpression) {
115+
return fail(node, matchExpression);
116+
}
117+
118+
@Override
119+
public Void visitForInLoop(ForInLoopTree node, String matchExpression) {
120+
return fail(node, matchExpression);
121+
}
122+
123+
@Override
124+
public Void visitForLoop(ForLoopTree node, String matchExpression) {
125+
return fail(node, matchExpression);
126+
}
127+
128+
@Override
129+
public Void visitForOfLoop(ForOfLoopTree node, String matchExpression) {
130+
return fail(node, matchExpression);
131+
}
132+
133+
@Override
134+
public Void visitFunctionCall(FunctionCallTree node, String matchExpression) {
135+
return fail(node, matchExpression);
136+
}
137+
138+
@Override
139+
public Void visitFunctionDeclaration(FunctionDeclarationTree node, String matchExpression) {
140+
return fail(node, matchExpression);
141+
}
142+
143+
@Override
144+
public Void visitFunctionExpression(FunctionExpressionTree node, String matchExpression) {
145+
return fail(node, matchExpression);
146+
}
147+
148+
@Override
149+
public Void visitImportEntry(ImportEntryTree node, String matchExpression) {
150+
return fail(node, matchExpression);
151+
}
152+
153+
@Override
154+
public Void visitInstanceOf(InstanceOfTree node, String matchExpression) {
155+
return fail(node, matchExpression);
156+
}
157+
158+
@Override
159+
public Void visitLabeledStatement(LabeledStatementTree node, String matchExpression) {
160+
return fail(node, matchExpression);
161+
}
162+
163+
@Override
164+
public Void visitModule(ModuleTree node, String matchExpression) {
165+
return fail(node, matchExpression);
166+
}
167+
168+
@Override
169+
public Void visitNew(NewTree node, String matchExpression) {
170+
return fail(node, matchExpression);
171+
}
172+
173+
@Override
174+
public Void visitRegExpLiteral(RegExpLiteralTree node, String matchExpression) {
175+
return fail(node, matchExpression);
176+
}
177+
178+
@Override
179+
public Void visitReturn(ReturnTree node, String matchExpression) {
180+
return fail(node, matchExpression);
181+
}
182+
183+
@Override
184+
public Void visitSpread(SpreadTree node, String matchExpression) {
185+
return fail(node, matchExpression);
186+
}
187+
188+
@Override
189+
public Void visitUnary(UnaryTree node, String matchExpression) {
190+
return fail(node, matchExpression);
191+
}
192+
193+
@Override
194+
public Void visitUnknown(Tree node, String matchExpression) {
195+
return fail(node, matchExpression);
196+
}
197+
198+
@Override
199+
public Void visitWhileLoop(WhileLoopTree node, String matchExpression) {
200+
return fail(node, matchExpression);
201+
}
202+
203+
@Override
204+
public Void visitWith(WithTree node, String matchExpression) {
205+
return fail(node, matchExpression);
206+
}
207+
208+
@Override
209+
public Void visitYield(YieldTree node, String matchExpression) {
210+
return fail(node, matchExpression);
211+
}
212+
213+
@Override
214+
public Void visitContinue(ContinueTree node, String matchExpression) {
215+
return fail(node, matchExpression);
216+
}
217+
218+
@Override
219+
public Void visitDebugger(DebuggerTree node, String matchExpression) {
220+
return fail(node, matchExpression);
221+
}
222+
223+
@Override
224+
public Void visitThrow(ThrowTree node, String matchExpression) {
225+
return fail(node, matchExpression);
226+
}
227+
228+
@Override
229+
public Void visitTry(TryTree node, String matchExpression) {
230+
return fail(node, matchExpression);
231+
}
232+
}

0 commit comments

Comments
 (0)