Skip to content

Commit 63d07af

Browse files
committed
solves #35: allow to also whitelist source paths
1 parent fb276be commit 63d07af

File tree

6 files changed

+126
-51
lines changed

6 files changed

+126
-51
lines changed

aem-classification-validator/README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ The following options are supported apart from the default settings mentioned in
1414

1515
Option | Mandatory | Description
1616
--- | --- | ---
17-
maps | yes | a comma-separated list of URLs specifying the source for a classification map. Each URL might use the protocols `file:`, for file-based classification maps, `http(s):` for classification maps in the internet or `tccl:` for classification maps being provided via the ThreadContextClassloader. The latter is especially useful with Maven as the TCCL during the execution of a goal of a Maven Plugin is the [Maven Plugin Classpath][4].
18-
whitelistedResourcePathPatterns | no | a comma-separated list of regular expressions matching an absolute resource path which should not be reported (no matter if its usage violates content classifications or not). The path is referring to the referenced/inherited/overlaid resource path (not the path containing the reference/supertype/overlay).
19-
severitiesPerClassification | no | the severity per classification (this will overwrite the default severity which otherwise used for all classifications). The format is `<classification>=<severity>{,<classification>=<severity>}`, where `classification` is one of `INTERNAL`, `INTERNAL_DEPRECATED_ANNOTATION`, `INTERNAL_DEPRECATED`, `FINAL` or `ABSTRACT` and `severity` is one of `DEBUG`, `INFO`, `WARN` or `ERROR`.
17+
maps | yes | a comma-separated list of URLs specifying the source for a classification map. Each URL might use the protocols `file:`, for file-based classification maps, `http(s):` for classification maps in the internet or `tccl:` for classification maps being provided via the ThreadContextClassloader. The latter is especially useful with Maven as the TCCL during the execution of a goal of a Maven Plugin is the [Maven Plugin Classpath][4].
18+
whitelistedResourcePathPatterns | no | a comma-separated list of regular expressions matching an absolute resource path which should not be reported (no matter if its usage violates content classifications or not). The path is referring to the referenced/inherited/overlaid resource path (not the path containing the reference/supertype/overlay).
19+
whitelistedSourcePathPatterns | no | a comma-separated list of regular expressions matching an absolute source path which should not be reported (no matter if its usage violates content classifications or not). Use this if you know there is an issue with classification for a specific component, but you don't want the problem to spread to other components.
20+
severitiesPerClassification | no | the severity per classification (this will overwrite the default severity which otherwise used for all classifications). The format is `<classification>=<severity>{,<classification>=<severity>}`, where `classification` is one of `INTERNAL`, `INTERNAL_DEPRECATED_ANNOTATION`, `INTERNAL_DEPRECATED`, `FINAL` or `ABSTRACT` and `severity` is one of `DEBUG`, `INFO`, `WARN` or `ERROR`.
2021

2122
All validation messages are emitted with the [`defaultSeverity`][2]
2223

@@ -33,16 +34,16 @@ The file is a CSV serialization of the map where each line represents one item i
3334
<path>,<classification>(,<remark>)
3435
```
3536

36-
where `classification` is one of
37+
where `classification` is one of
3738

3839
1. `INTERNAL`
3940
2. `INTERNAL_DEPRECATED_ANNOTATION`, same restrictions as `INTERNAL` but due to being marked as deprecated via some annotation e.g. `cq:deprecated` property
4041
3. `INTERNAL_DEPRECATED`, same restrictions as `INTERNAL` but due to being marked as deprecated in some external sources like release notes
4142
4. `FINAL`
4243
5. `ABSTRACT`
43-
6. `PUBLIC`
44+
6. `PUBLIC`
4445

45-
(in order from most restricted to least restricted).
46+
(in order from most restricted to least restricted).
4647
The explanation for those can be found in the [Adobe documentation][1].
4748
The CSV format is based on [RFC 4180][7]. In addition a comment starting with `#` on the first line is supposed to contain a label for the map (like the underlying AEM version). `path` is supposed to be an absolute JCR path of a specific node.
4849

@@ -94,8 +95,8 @@ There are several reasons:
9495

9596
1. You should detect violations as early as possible, preferably already in your CI pipeline. The later you detect those the more effort it is to fix.
9697
2. If you don't care about content classifications
97-
1. there is a high chance that you cannot easily upgrade to a newer AEM version (AMS or on-premise)
98-
2. it might break with every new [AEM as a Cloud Service][5] release
98+
1. there is a high chance that you cannot easily upgrade to a newer AEM version (AMS or on-premise)
99+
2. it might break with every new [AEM as a Cloud Service][5] release
99100

100101
[1]: https://docs.adobe.com/content/help/en/experience-manager-65/deploying/upgrading/sustainable-upgrades.html#content-classifications
101102
[2]: https://jackrabbit.apache.org/filevault/validation.html

aem-classification-validator/pom.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@
9292
<version>2.2</version>
9393
<scope>test</scope>
9494
</dependency>
95+
<dependency>
96+
<groupId>org.mockito</groupId>
97+
<artifactId>mockito-core</artifactId>
98+
<version>5.20.0</version>
99+
<scope>test</scope>
100+
</dependency>
95101
<!-- only transitive dependencies of 'vault-validation' but must be declared due to https://issues.apache.org/jira/browse/JCRVLT-394 -->
96102
<dependency>
97103
<groupId>javax.jcr</groupId>
@@ -112,4 +118,4 @@
112118
<scope>test</scope>
113119
</dependency>
114120
</dependencies>
115-
</project>
121+
</project>

aem-classification-validator/src/main/java/biz/netcentric/filevault/validator/aem/classification/AemClassificationValidator.java

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,21 +41,25 @@
4141
import org.apache.sling.jcr.resource.api.JcrResourceConstants;
4242
import org.jetbrains.annotations.NotNull;
4343
import org.jetbrains.annotations.Nullable;
44+
import org.slf4j.Logger;
45+
import org.slf4j.LoggerFactory;
4446

4547
public class AemClassificationValidator implements DocumentViewXmlValidator, GenericJcrDataValidator, NodePathValidator {
4648

49+
private static final Logger LOGGER = LoggerFactory.getLogger(AemClassificationValidator.class);
50+
4751
/**
4852
* Example HTL code which should be matched by the RegEx:
4953
* <br>
5054
* <code>&lt;article data-sly-resource="${'path/to/resource' @ resourceType='&lt;some resource type&gt;'}"&gt;</code>
5155
* <br>
5256
* The first subgroup must contain the value of the resource type.
5357
* This pattern only works if the resource type is given as literal!
54-
*
58+
*
5559
* @see <a href="https://helpx.adobe.com/experience-manager/htl/using/block-statements.html#resource">data-sly-resource</a>
5660
*/
5761
static final Pattern HTL_INCLUDE_OVERWRITING_RESOURCE_TYPE = Pattern.compile("data-sly-resource\\s*=[^@]*.*?resourceType\\s*=\\s*(?:\"|\')([^'\"]*)(?:\"|\')");
58-
62+
5963
/**
6064
* Example JSP code which should be matched by the RegEx:
6165
* <br>
@@ -64,7 +68,7 @@ public class AemClassificationValidator implements DocumentViewXmlValidator, Gen
6468
* <br>
6569
* The first subgroup must contain the value of the resource type.
6670
* This pattern only works if the resource type is given as literal!
67-
*
71+
*
6872
* @see <a href="https://experienceleague.adobe.com/docs/experience-manager-65/developing/platform/taglib.html?lang=en">CQ/Sling Tag Library</a>
6973
*/
7074
private static final Pattern JSP_INCLUDE_OVERWRITING_RESOURCE_TYPE = Pattern.compile("(?:<cq:|<sling:)include resourceType\\s*=\\s*(?:\"|\')([^'\"]*)(?:\"|\')");
@@ -84,18 +88,22 @@ public class AemClassificationValidator implements DocumentViewXmlValidator, Gen
8488

8589
private final ContentClassificationMap classificationMap;
8690
private final Collection<String> whitelistedResourcePaths;
91+
private final Collection<String> whitelistedSourcePaths;
8792
private final Collection<Pattern> whitelistedResourcePathPatterns;
93+
private final Collection<Pattern> whitelistedSourcePathPatterns;
8894
private final Map<ContentClassification, ValidationMessageSeverity> severityPerClassification;
8995

9096
private @NotNull ValidationMessageSeverity defaultSeverity;
9197
private final Collection<String> overlaidNodePaths;
9298

93-
public AemClassificationValidator(@NotNull ValidationMessageSeverity defaultSeverity, @NotNull ContentClassificationMap classificationMap, @NotNull Collection<String> whitelistedResourcePaths, @NotNull Map<ContentClassification, ValidationMessageSeverity> severityPerClassification) {
99+
public AemClassificationValidator(@NotNull ValidationMessageSeverity defaultSeverity, @NotNull ContentClassificationMap classificationMap, @NotNull Collection<String> whitelistedResourcePaths, @NotNull Collection<String> whitelistedSourcePaths, @NotNull Map<ContentClassification, ValidationMessageSeverity> severityPerClassification) {
94100
super();
95101
this.defaultSeverity = defaultSeverity;
96102
this.classificationMap = classificationMap;
97103
this.whitelistedResourcePaths = whitelistedResourcePaths;
98104
this.whitelistedResourcePathPatterns = whitelistedResourcePaths.stream().map(Pattern::compile).collect(Collectors.toList());
105+
this.whitelistedSourcePaths = whitelistedSourcePaths;
106+
this.whitelistedSourcePathPatterns = whitelistedSourcePaths.stream().map(Pattern::compile).collect(Collectors.toList());
99107
this.severityPerClassification = severityPerClassification;
100108
this.overlaidNodePaths = new LinkedList<>();
101109
}
@@ -106,6 +114,11 @@ public Collection<ValidationMessage> done() {
106114

107115
@Override
108116
public Collection<ValidationMessage> validate(@NotNull String path) {
117+
if (isSourcePathWhitelisted(path, whitelistedSourcePathPatterns)) {
118+
LOGGER.debug("Source path '{}' is explicitly whitelisted and therefore has no restrictions!", path);
119+
return null;
120+
}
121+
109122
if (!overlaidNodePaths.contains(path)) {
110123
// check overlay usage in addition for non-docview files
111124
ValidationMessage message = validateClassification(path, ContentUsage.OVERLAY, MESSAGE_SUBJECT_FILE);
@@ -118,6 +131,10 @@ public Collection<ValidationMessage> validate(@NotNull String path) {
118131

119132
@Override
120133
public boolean shouldValidateJcrData(@NotNull Path filePath) {
134+
if (isSourcePathWhitelisted(filePath.toString(), whitelistedSourcePathPatterns)) {
135+
LOGGER.debug("Source path '{}' is explicitly whitelisted and therefore has no restrictions!", filePath);
136+
return false;
137+
}
121138
return (isHtlFile(filePath) || isJspFile(filePath));
122139
}
123140

@@ -167,6 +184,11 @@ static String jcrExpandedFormNameToReadableFormat(String jcrExpandedFormName) {
167184

168185
@Override
169186
public Collection<ValidationMessage> validate(@NotNull DocViewNode node, @NotNull String nodePath, @NotNull Path filePath, boolean isRoot) {
187+
if (isSourcePathWhitelisted(nodePath, whitelistedSourcePathPatterns)) {
188+
LOGGER.debug("Source path '{}' is explicitly whitelisted and therefore has no restrictions!", nodePath);
189+
return null;
190+
}
191+
170192
Collection<ValidationMessage> messages = new LinkedList<>();
171193
String subject = String.format(MESSAGE_SUBJECT_NODE, jcrExpandedFormNameToReadableFormat(node.label));
172194

@@ -190,7 +212,7 @@ public Collection<ValidationMessage> validate(@NotNull DocViewNode node, @NotNul
190212
messages.add(message);
191213
overlaidNodePaths.add(nodePath);
192214
}
193-
215+
194216
// TODO: check usage of clientlib dependencies/embeds
195217
return messages;
196218
}
@@ -204,7 +226,7 @@ public Collection<ValidationMessage> validate(@NotNull DocViewNode node, @NotNul
204226
// add subject and usage to message
205227
return new ValidationMessage(defaultSeverity, "Resource path must not end with '/' but is '" + resourcePath + "'");
206228
}
207-
229+
208230
if (usage == ContentUsage.OVERLAY) {
209231
if (!resourcePath.startsWith("/apps/")) {
210232
return null; // this is not an overlay at all, therefore no violation
@@ -228,26 +250,34 @@ private static boolean isJspFile(Path file) {
228250
return JSP_PATH_MATCHER.matches(file);
229251
}
230252

253+
private static boolean isSourcePathWhitelisted(@NotNull String sourcePath, @Nullable Collection<Pattern> whitelistedSourcePathPatterns) {
254+
if (whitelistedSourcePathPatterns == null) {
255+
return false;
256+
}
257+
return whitelistedSourcePathPatterns.stream().anyMatch(r -> r.matcher(sourcePath).matches());
258+
}
259+
231260
static @NotNull String extendMessageWithRemark(@NotNull String message, String remark) {
232261
if (remark != null && !remark.isEmpty()) {
233262
return message + " Remark: " + remark;
234263
}
235264
return message;
236265
}
237-
266+
238267
@NotNull ValidationMessageSeverity getSeverityForClassification(ContentClassification classification) {
239268
ValidationMessageSeverity severity = severityPerClassification.get(classification);
240269
return severity != null ? severity : defaultSeverity;
241270
}
242271

243-
272+
244273
@Override
245274
public int hashCode() {
246275
final int prime = 31;
247276
int result = 1;
248277
result = prime * result + ((classificationMap == null) ? 0 : classificationMap.hashCode());
249278
result = prime * result + ((defaultSeverity == null) ? 0 : defaultSeverity.hashCode());
250279
result = prime * result + ((whitelistedResourcePaths == null) ? 0 : whitelistedResourcePaths.hashCode());
280+
result = prime * result + ((whitelistedSourcePaths == null) ? 0 : whitelistedSourcePaths.hashCode());
251281
result = prime * result + ((severityPerClassification == null) ? 0 : severityPerClassification.hashCode());
252282
return result;
253283
}
@@ -273,6 +303,11 @@ public boolean equals(Object obj) {
273303
return false;
274304
} else if (!whitelistedResourcePaths.equals(other.whitelistedResourcePaths))
275305
return false;
306+
if (whitelistedSourcePaths == null) {
307+
if (other.whitelistedSourcePaths != null)
308+
return false;
309+
} else if (!whitelistedSourcePaths.equals(other.whitelistedSourcePaths))
310+
return false;
276311
if (severityPerClassification == null) {
277312
if (other.severityPerClassification != null)
278313
return false;
@@ -285,6 +320,7 @@ public boolean equals(Object obj) {
285320
public String toString() {
286321
return "AemClassificationValidator [" + (classificationMap != null ? "classificationMap=" + classificationMap + ", " : "")
287322
+ (whitelistedResourcePaths != null ? "resourceTypeWhitelist=" + whitelistedResourcePaths + ", " : "")
323+
+ (whitelistedSourcePaths != null ? "sourcePathWhitelist=" + whitelistedSourcePaths + ", " : "")
288324
+ (severityPerClassification != null ? "severityPerClassification=" + severityPerClassification + ", " : "")
289325
+ (defaultSeverity != null ? "defaultSeverity=" + defaultSeverity : "") + "]";
290326
}

aem-classification-validator/src/main/java/biz/netcentric/filevault/validator/aem/classification/AemClassificationValidatorFactory.java

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ public class AemClassificationValidatorFactory implements ValidatorFactory {
5252
private static final String OPTION_WHITELISTED_RESOURCE_PATH_PATTERNS = "whitelistedResourcePathPatterns";
5353
private static final String OPTION_WHITELISTED_RESOURCE_PATH_PATTERNS_OLD = "whitelistedResourcePathsPatterns";
5454

55+
/** optional list of comma-separated source path patterns (should be absolute) */
56+
private static final String OPTION_WHITELISTED_SOURCE_PATH_PATTERNS = "whitelistedSourcePathPatterns";
57+
5558
private static final Object OPTION_SEVERITIES_PER_CLASSIFICATION = "severitiesPerClassification";
5659

5760
private static final Logger LOGGER = LoggerFactory.getLogger(AemClassificationValidatorFactory.class);
@@ -72,18 +75,14 @@ public Validator createValidator(@NotNull ValidationContext context, @NotNull Va
7275
optionWhitelistedResourcePaths = settings.getOptions().get(OPTION_WHITELISTED_RESOURCE_PATH_PATTERNS);
7376
}
7477

75-
Collection<String> whitelistedResourcePaths =
76-
Optional.ofNullable(optionWhitelistedResourcePaths)
77-
.map(op -> Arrays.stream(op.split(","))
78-
.map(String::trim)
79-
.collect(Collectors.toList()))
80-
.orElse(Collections.emptyList());
81-
82-
try {
83-
whitelistedResourcePaths.stream().forEach(AemClassificationValidatorFactory::validateResourcePathPattern);
84-
} catch (IllegalArgumentException e) {
85-
throw new IllegalArgumentException("At least one value given in option " + OPTION_WHITELISTED_RESOURCE_PATH_PATTERNS + " is invalid", e);
78+
String optionWhitelistedSourcePaths = null;
79+
if (settings.getOptions().containsKey(OPTION_WHITELISTED_SOURCE_PATH_PATTERNS)) {
80+
optionWhitelistedSourcePaths = settings.getOptions().get(OPTION_WHITELISTED_SOURCE_PATH_PATTERNS);
8681
}
82+
83+
Collection<String> whitelistedResourcePaths = getPathsFromOption(optionWhitelistedResourcePaths);
84+
Collection<String> whitelistedSourcePaths = getPathsFromOption(optionWhitelistedSourcePaths);
85+
8786
try {
8887
Collection<ContentClassificationMap> maps = new LinkedList<>();
8988
for (String mapUrl : mapUrls.split("\\s*,\\s*")) {
@@ -96,7 +95,7 @@ public Validator createValidator(@NotNull ValidationContext context, @NotNull Va
9695
throw new IllegalArgumentException("At least one valid map must be given!");
9796
}
9897
return new AemClassificationValidator(settings.getDefaultSeverity(), new CompositeContentClassificationMap(maps), whitelistedResourcePaths,
99-
getSeverityPerClassification(settings.getOptions().get(OPTION_SEVERITIES_PER_CLASSIFICATION)));
98+
whitelistedSourcePaths, getSeverityPerClassification(settings.getOptions().get(OPTION_SEVERITIES_PER_CLASSIFICATION)));
10099
} catch (IOException e) {
101100
throw new IllegalStateException("Could not read from " + mapUrls, e);
102101
}
@@ -141,6 +140,22 @@ private static Map<ContentClassification, ValidationMessageSeverity> parseSeveri
141140
return result;
142141
}
143142

143+
private static Collection<String> getPathsFromOption(String optionPaths) {
144+
Collection<String> result =
145+
Optional.ofNullable(optionPaths)
146+
.map(op -> Arrays.stream(op.split(","))
147+
.map(String::trim)
148+
.collect(Collectors.toList()))
149+
.orElse(Collections.emptyList());
150+
151+
try {
152+
result.stream().forEach(AemClassificationValidatorFactory::validateResourcePathPattern);
153+
} catch (IllegalArgumentException e) {
154+
throw new IllegalArgumentException("At least one value given in option " + OPTION_WHITELISTED_RESOURCE_PATH_PATTERNS + " is invalid", e);
155+
}
156+
return result;
157+
}
158+
144159
static void validateResourcePathPattern(String resourcePathPattern) throws IllegalArgumentException {
145160
Matcher matcher = Pattern.compile(resourcePathPattern).matcher("/");
146161
matcher.matches();

0 commit comments

Comments
 (0)