This is an automated email from the ASF dual-hosted git repository.
pvillard31 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new af5138f47fc NIFI-16329 Added Coordinate Validation for Registry NAR
Bundles (#11662)
af5138f47fc is described below
commit af5138f47fc99832f0fd5e00d9e0f86a9d0d9a14
Author: David Handermann <[email protected]>
AuthorDate: Wed Sep 9 13:46:45 2026 -0500
NIFI-16329 Added Coordinate Validation for Registry NAR Bundles (#11662)
---
.../bundle/extract/nar/NarBundleExtractor.java | 34 ++++-------
.../registry/bundle/model/BundleIdentifier.java | 8 +--
.../nifi/registry/bundle/util/BundleUtils.java | 12 ++++
.../bundle/extract/nar/TestNarBundleExtractor.java | 28 ++++++++-
.../bundle/model/TestBundleIdentifier.java | 41 +++++++++++++
.../nifi/registry/bundle/util/TestBundleUtils.java | 43 ++++++++++++++
.../FileSystemBundlePersistenceProvider.java | 25 ++++----
.../extension/StandardBundleCoordinate.java | 4 ++
.../extension/StandardBundleVersionCoordinate.java | 5 ++
.../flow/FileSystemFlowPersistenceProvider.java | 24 +++-----
.../flow/git/GitFlowPersistenceProvider.java | 21 +++++--
.../nifi/registry/service/RegistryService.java | 24 ++++++--
.../extension/StandardExtensionService.java | 35 ++++++++---
.../TestFileSystemBundlePersistenceProvider.java | 57 +++++++++++++++++-
.../extension/TestStandardBundleCoordinate.java | 57 ++++++++++++++++++
.../TestStandardBundleVersionCoordinate.java | 67 ++++++++++++++++++++++
.../TestFileSystemFlowPersistenceProvider.java | 12 ++++
.../flow/git/TestGitFlowPersistenceProvider.java | 29 ++++++++++
.../nifi/registry/service/TestRegistryService.java | 27 +++++++++
.../org/apache/nifi/registry/util/FileUtils.java | 27 +++++++++
.../apache/nifi/registry/util/TestFileUtils.java | 30 ++++++++++
.../registry/aws/S3BundlePersistenceProvider.java | 7 ++-
22 files changed, 533 insertions(+), 84 deletions(-)
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/nar/NarBundleExtractor.java
b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/nar/NarBundleExtractor.java
index c06030d56af..72fd3219bbe 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/nar/NarBundleExtractor.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/nar/NarBundleExtractor.java
@@ -134,33 +134,21 @@ public class NarBundleExtractor implements
BundleExtractor {
}
private BundleIdentifier getBundleCoordinate(final Attributes attributes) {
- try {
- final String groupId =
attributes.getValue(NarManifestEntry.NAR_GROUP.getManifestName());
- final String artifactId =
attributes.getValue(NarManifestEntry.NAR_ID.getManifestName());
- final String version =
attributes.getValue(NarManifestEntry.NAR_VERSION.getManifestName());
-
- return new BundleIdentifier(groupId, artifactId, version);
- } catch (Exception e) {
- throw new BundleException("Unable to obtain bundle coordinate due
to: " + e.getMessage(), e);
- }
+ final String groupId =
attributes.getValue(NarManifestEntry.NAR_GROUP.getManifestName());
+ final String artifactId =
attributes.getValue(NarManifestEntry.NAR_ID.getManifestName());
+ final String version =
attributes.getValue(NarManifestEntry.NAR_VERSION.getManifestName());
+ return new BundleIdentifier(groupId, artifactId, version);
}
private BundleIdentifier getDependencyBundleCoordinate(final Attributes
attributes) {
- try {
- final String dependencyGroupId =
attributes.getValue(NarManifestEntry.NAR_DEPENDENCY_GROUP.getManifestName());
- final String dependencyArtifactId =
attributes.getValue(NarManifestEntry.NAR_DEPENDENCY_ID.getManifestName());
- final String dependencyVersion =
attributes.getValue(NarManifestEntry.NAR_DEPENDENCY_VERSION.getManifestName());
-
- final BundleIdentifier dependencyCoordinate;
- if (dependencyArtifactId != null) {
- dependencyCoordinate = new BundleIdentifier(dependencyGroupId,
dependencyArtifactId, dependencyVersion);
- } else {
- dependencyCoordinate = null;
- }
- return dependencyCoordinate;
- } catch (Exception e) {
- throw new BundleException("Unable to obtain bundle coordinate for
dependency due to: " + e.getMessage(), e);
+ final String dependencyGroupId =
attributes.getValue(NarManifestEntry.NAR_DEPENDENCY_GROUP.getManifestName());
+ final String dependencyArtifactId =
attributes.getValue(NarManifestEntry.NAR_DEPENDENCY_ID.getManifestName());
+ final String dependencyVersion =
attributes.getValue(NarManifestEntry.NAR_DEPENDENCY_VERSION.getManifestName());
+ if (dependencyArtifactId == null) {
+ return null;
}
+
+ return new BundleIdentifier(dependencyGroupId, dependencyArtifactId,
dependencyVersion);
}
private BuildInfo getBuildInfo(final Attributes attributes) {
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/model/BundleIdentifier.java
b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/model/BundleIdentifier.java
index 8b9e1361cc9..d6cffc70255 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/model/BundleIdentifier.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/model/BundleIdentifier.java
@@ -16,7 +16,7 @@
*/
package org.apache.nifi.registry.bundle.model;
-import static
org.apache.nifi.registry.bundle.util.BundleUtils.validateNotBlank;
+import org.apache.nifi.registry.bundle.util.BundleUtils;
/**
* The identifier of an extension bundle (i.e group + artifact + version).
@@ -33,9 +33,9 @@ public class BundleIdentifier {
this.groupId = groupId;
this.artifactId = artifactId;
this.version = version;
- validateNotBlank("Group Id", this.groupId);
- validateNotBlank("Artifact Id", this.artifactId);
- validateNotBlank("Version", this.version);
+ BundleUtils.validateCoordinateField("Group Id", this.groupId);
+ BundleUtils.validateCoordinateField("Artifact Id", this.artifactId);
+ BundleUtils.validateCoordinateField("Version", this.version);
this.identifier = this.groupId + ":" + this.artifactId + ":" +
this.version;
}
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/util/BundleUtils.java
b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/util/BundleUtils.java
index 4c684ab9581..2a5bf95aa9f 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/util/BundleUtils.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/util/BundleUtils.java
@@ -34,4 +34,16 @@ public class BundleUtils {
}
}
+ public static void validateCoordinateField(final String fieldName, final
String value) {
+ validateNotBlank(fieldName, value);
+
+ if (".".equals(value) || "..".equals(value)) {
+ throw new IllegalArgumentException(fieldName + " is not a valid
coordinate field");
+ }
+
+ if (value.indexOf('/') >= 0 || value.indexOf('\\') >= 0 ||
value.indexOf('\0') >= 0) {
+ throw new IllegalArgumentException(fieldName + " contains invalid
characters");
+ }
+ }
+
}
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/bundle/extract/nar/TestNarBundleExtractor.java
b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/bundle/extract/nar/TestNarBundleExtractor.java
index a7f18f22b6c..804e3f4488a 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/bundle/extract/nar/TestNarBundleExtractor.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/bundle/extract/nar/TestNarBundleExtractor.java
@@ -97,7 +97,15 @@ public class TestNarBundleExtractor {
@Test
public void testExtractFromNarMissingRequiredManifestEntries() throws
IOException {
try (final InputStream in = new
FileInputStream("src/test/resources/nars/nifi-missing-manifest-entries.nar")) {
- assertThrows(BundleException.class, () -> extractor.extract(in));
+ assertThrows(IllegalArgumentException.class, () ->
extractor.extract(in));
+ }
+ }
+
+ @Test
+ public void testExtractFromNarWithParentDirectoryCoordinates(@TempDir
final Path tempDir) throws IOException {
+ final Path narPath = writeNar(tempDir, "..", "..", "1.0.0");
+ try (final InputStream in = Files.newInputStream(narPath)) {
+ assertThrows(IllegalArgumentException.class, () ->
extractor.extract(in));
}
}
@@ -200,4 +208,22 @@ public class TestNarBundleExtractor {
}
}
+ private Path writeNar(final Path tempDir, final String groupId, final
String artifactId, final String version) throws IOException {
+ final Path narPath = tempDir.resolve("testing.nar");
+ try (final JarOutputStream jarOutputStream = new
JarOutputStream(Files.newOutputStream(narPath))) {
+ final JarEntry manifestEntry = new
JarEntry("META-INF/MANIFEST.MF");
+ jarOutputStream.putNextEntry(manifestEntry);
+ jarOutputStream.write((
+ "Manifest-Version: 1.0\n" +
+ "Nar-Group: " + groupId + "\n" +
+ "Nar-Id: " + artifactId + "\n" +
+ "Nar-Version: " + version + "\n" +
+ "Build-Timestamp: 2024-01-01T00:00:00Z\n\n"
+ ).getBytes(StandardCharsets.UTF_8));
+ jarOutputStream.closeEntry();
+ }
+
+ return narPath;
+ }
+
}
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/bundle/model/TestBundleIdentifier.java
b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/bundle/model/TestBundleIdentifier.java
new file mode 100644
index 00000000000..aebddad057b
--- /dev/null
+++
b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/bundle/model/TestBundleIdentifier.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.
+ */
+package org.apache.nifi.registry.bundle.model;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class TestBundleIdentifier {
+
+ @Test
+ void testValidIdentifier() {
+ final BundleIdentifier identifier = new
BundleIdentifier("org.apache.nifi", "nifi-standard-nar", "2.0.0-SNAPSHOT");
+ assertEquals("org.apache.nifi", identifier.getGroupId());
+ assertEquals("nifi-standard-nar", identifier.getArtifactId());
+ assertEquals("2.0.0-SNAPSHOT", identifier.getVersion());
+ }
+
+ @Test
+ void testRejectsInvalidComponents() {
+ assertThrows(IllegalArgumentException.class, () -> new
BundleIdentifier("..", "nifi-standard-nar", "1.0.0"));
+ assertThrows(IllegalArgumentException.class, () -> new
BundleIdentifier("org.apache.nifi", "..", "1.0.0"));
+ assertThrows(IllegalArgumentException.class, () -> new
BundleIdentifier("org.apache.nifi", "nifi-standard-nar", ".."));
+ assertThrows(IllegalArgumentException.class, () -> new
BundleIdentifier("org/apache", "nifi-standard-nar", "1.0.0"));
+ }
+}
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/bundle/util/TestBundleUtils.java
b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/bundle/util/TestBundleUtils.java
new file mode 100644
index 00000000000..87d960bca2d
--- /dev/null
+++
b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/bundle/util/TestBundleUtils.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.
+ */
+package org.apache.nifi.registry.bundle.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class TestBundleUtils {
+
+ @Test
+ void testValidateCoordinateFieldAcceptsTypicalValues() {
+ BundleUtils.validateCoordinateField("Group Id", "org.apache.nifi");
+ BundleUtils.validateCoordinateField("Artifact Id",
"nifi-standard-nar");
+ BundleUtils.validateCoordinateField("Version", "2.0.0-SNAPSHOT");
+ BundleUtils.validateCoordinateField("Version", "1.0.0+build.5");
+ }
+
+ @Test
+ void testValidateCoordinateFieldRejectsInvalidValues() {
+ assertThrows(IllegalArgumentException.class, () ->
BundleUtils.validateCoordinateField("Group Id", null));
+ assertThrows(IllegalArgumentException.class, () ->
BundleUtils.validateCoordinateField("Group Id", " "));
+ assertThrows(IllegalArgumentException.class, () ->
BundleUtils.validateCoordinateField("Group Id", "."));
+ assertThrows(IllegalArgumentException.class, () ->
BundleUtils.validateCoordinateField("Group Id", ".."));
+ assertThrows(IllegalArgumentException.class, () ->
BundleUtils.validateCoordinateField("Group Id", "org/apache"));
+ assertThrows(IllegalArgumentException.class, () ->
BundleUtils.validateCoordinateField("Artifact Id", "art\\ifact"));
+ assertThrows(IllegalArgumentException.class, () ->
BundleUtils.validateCoordinateField("Version", "1.0.0\0"));
+ }
+}
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/FileSystemBundlePersistenceProvider.java
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/FileSystemBundlePersistenceProvider.java
index 0fe7e0d3891..f0883920b29 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/FileSystemBundlePersistenceProvider.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/FileSystemBundlePersistenceProvider.java
@@ -186,14 +186,14 @@ public class FileSystemBundlePersistenceProvider
implements BundlePersistencePro
// delete the directory for the group and bucket if there is nothing
left
final File groupDir = bundleDir.getParentFile();
final File[] groupFiles = groupDir.listFiles();
- if (groupFiles.length == 0) {
+ if (groupFiles != null && groupFiles.length == 0) {
final boolean deletedGroup = groupDir.delete();
if (!deletedGroup) {
LOGGER.error("Unable to delete group directory: {}",
groupDir.getAbsolutePath());
} else {
final File bucketDir = groupDir.getParentFile();
final File[] bucketFiles = bucketDir.listFiles();
- if (bucketFiles.length == 0) {
+ if (bucketFiles != null && bucketFiles.length == 0) {
final boolean deletedBucket = bucketDir.delete();
if (!deletedBucket) {
LOGGER.error("Unable to delete bucket directory: {}",
bucketDir.getAbsolutePath());
@@ -214,7 +214,7 @@ public class FileSystemBundlePersistenceProvider implements
BundlePersistencePro
final String artifactId = bundleCoordinate.getArtifactId();
final Path artifactPath = getArtifactPath(bucketId, groupId,
artifactId);
- return getChildLocation(bundleStorageDir, artifactPath);
+ return FileUtils.getChildLocation(bundleStorageDir, artifactPath);
}
static File getBundleVersionDirectory(final File bundleStorageDir, final
BundleVersionCoordinate versionCoordinate) {
@@ -225,7 +225,7 @@ public class FileSystemBundlePersistenceProvider implements
BundlePersistencePro
final Path artifactPath = getArtifactPath(bucketId, groupId,
artifactId);
final Path versionPath = Paths.get(sanitize(version)).normalize();
- return getChildLocation(bundleStorageDir,
artifactPath.resolve(versionPath));
+ return FileUtils.getChildLocation(bundleStorageDir,
artifactPath.resolve(versionPath));
}
static File getBundleFile(final File parentDir, final
BundleVersionCoordinate versionCoordinate) {
@@ -235,7 +235,7 @@ public class FileSystemBundlePersistenceProvider implements
BundlePersistencePro
final String bundleFileExtension = getBundleFileExtension(bundleType);
final String bundleFilename = sanitize(artifactId) + "-" +
sanitize(version) + bundleFileExtension;
- return getChildLocation(parentDir, Paths.get(bundleFilename));
+ return FileUtils.getChildLocation(parentDir,
Paths.get(bundleFilename));
}
static Path getArtifactPath(final String bucketId, final String groupId,
final String artifactId) {
@@ -243,7 +243,12 @@ public class FileSystemBundlePersistenceProvider
implements BundlePersistencePro
}
static String sanitize(final String input) {
- return FileUtils.sanitizeFilename(input).trim().toLowerCase();
+ final String sanitized =
FileUtils.sanitizeFilename(input).trim().toLowerCase();
+ if (".".equals(sanitized) || "..".equals(sanitized)) {
+ throw new IllegalArgumentException("Coordinate component is not a
valid path name");
+ }
+
+ return sanitized;
}
static String getBundleFileExtension(final BundleVersionType bundleType) {
@@ -265,12 +270,4 @@ public class FileSystemBundlePersistenceProvider
implements BundlePersistencePro
}
}
- private static File getChildLocation(final File parentDir, final Path
childLocation) {
- final Path parentPath = parentDir.toPath().normalize();
- final Path childPath = parentPath.resolve(childLocation.normalize());
- if (childPath.startsWith(parentPath)) {
- return childPath.toFile();
- }
- throw new IllegalArgumentException(String.format("Child location not
valid [%s]", childLocation));
- }
}
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardBundleCoordinate.java
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardBundleCoordinate.java
index a109d9ecefa..337e2c9aff5 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardBundleCoordinate.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardBundleCoordinate.java
@@ -17,6 +17,7 @@
package org.apache.nifi.registry.provider.extension;
import org.apache.commons.lang3.Validate;
+import org.apache.nifi.registry.bundle.util.BundleUtils;
import org.apache.nifi.registry.extension.BundleCoordinate;
import java.util.Objects;
@@ -34,6 +35,9 @@ public class StandardBundleCoordinate implements
BundleCoordinate {
Validate.notBlank(this.bucketId, "Bucket Id is required");
Validate.notBlank(this.groupId, "Group Id is required");
Validate.notBlank(this.artifactId, "Artifact Id is required");
+ BundleUtils.validateCoordinateField("Bucket Id", this.bucketId);
+ BundleUtils.validateCoordinateField("Group Id", this.groupId);
+ BundleUtils.validateCoordinateField("Artifact Id", this.artifactId);
}
@Override
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardBundleVersionCoordinate.java
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardBundleVersionCoordinate.java
index a3e076362d1..30913257aa5 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardBundleVersionCoordinate.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardBundleVersionCoordinate.java
@@ -17,6 +17,7 @@
package org.apache.nifi.registry.provider.extension;
import org.apache.commons.lang3.Validate;
+import org.apache.nifi.registry.bundle.util.BundleUtils;
import org.apache.nifi.registry.extension.BundleVersionCoordinate;
import org.apache.nifi.registry.extension.BundleVersionType;
@@ -41,6 +42,10 @@ public class StandardBundleVersionCoordinate implements
BundleVersionCoordinate
Validate.notBlank(this.artifactId, "Artifact Id is required");
Validate.notBlank(this.version, "Version is required");
Validate.notNull(this.type, "BundleVersionType is required");
+ BundleUtils.validateCoordinateField("Bucket Id", this.bucketId);
+ BundleUtils.validateCoordinateField("Group Id", this.groupId);
+ BundleUtils.validateCoordinateField("Artifact Id", this.artifactId);
+ BundleUtils.validateCoordinateField("Version", this.version);
}
@Override
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/FileSystemFlowPersistenceProvider.java
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/FileSystemFlowPersistenceProvider.java
index c8adfe8fb78..25228acb0a3 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/FileSystemFlowPersistenceProvider.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/FileSystemFlowPersistenceProvider.java
@@ -78,14 +78,14 @@ public class FileSystemFlowPersistenceProvider implements
FlowPersistenceProvide
@Override
public synchronized void saveFlowContent(final FlowSnapshotContext
context, final byte[] content) throws FlowPersistenceException {
- final File bucketDir = getChildLocation(flowStorageDir,
getNormalizedIdPath(context.getBucketId()));
+ final File bucketDir = FileUtils.getChildLocation(flowStorageDir,
getNormalizedIdPath(context.getBucketId()));
try {
FileUtils.ensureDirectoryExistAndCanReadAndWrite(bucketDir);
} catch (IOException e) {
throw new FlowPersistenceException("Error accessing bucket
directory at " + bucketDir.getAbsolutePath(), e);
}
- final File flowDir = getChildLocation(bucketDir,
getNormalizedIdPath(context.getFlowId()));
+ final File flowDir = FileUtils.getChildLocation(bucketDir,
getNormalizedIdPath(context.getFlowId()));
try {
FileUtils.ensureDirectoryExistAndCanReadAndWrite(flowDir);
} catch (IOException e) {
@@ -93,7 +93,7 @@ public class FileSystemFlowPersistenceProvider implements
FlowPersistenceProvide
}
final String versionString = String.valueOf(context.getVersion());
- final File versionDir = getChildLocation(flowDir,
Paths.get(versionString));
+ final File versionDir = FileUtils.getChildLocation(flowDir,
Paths.get(versionString));
try {
FileUtils.ensureDirectoryExistAndCanReadAndWrite(versionDir);
} catch (IOException e) {
@@ -101,7 +101,7 @@ public class FileSystemFlowPersistenceProvider implements
FlowPersistenceProvide
}
final String versionExtension = versionString + SNAPSHOT_EXTENSION;
- final File versionFile = getChildLocation(versionDir,
Paths.get(versionExtension));
+ final File versionFile = FileUtils.getChildLocation(versionDir,
Paths.get(versionExtension));
if (versionFile.exists()) {
throw new FlowPersistenceException("Unable to save, a snapshot
already exists with version " + versionString);
}
@@ -141,7 +141,7 @@ public class FileSystemFlowPersistenceProvider implements
FlowPersistenceProvide
final Path bucketIdPath = getNormalizedIdPath(bucketId);
final Path flowIdPath = getNormalizedIdPath(flowId);
final Path bucketFlowPath = bucketIdPath.resolve(flowIdPath);
- final File flowDir = getChildLocation(flowStorageDir, bucketFlowPath);
+ final File flowDir = FileUtils.getChildLocation(flowStorageDir,
bucketFlowPath);
if (!flowDir.exists()) {
LOGGER.debug("Snapshot directory does not exist at {}",
flowDir.getAbsolutePath());
return;
@@ -161,7 +161,7 @@ public class FileSystemFlowPersistenceProvider implements
FlowPersistenceProvide
}
// delete the directory for the bucket if there is nothing left
- final File bucketDir = getChildLocation(flowStorageDir,
getNormalizedIdPath(bucketId));
+ final File bucketDir = FileUtils.getChildLocation(flowStorageDir,
getNormalizedIdPath(bucketId));
final File[] bucketFiles = bucketDir.listFiles();
if (bucketFiles == null || bucketFiles.length == 0) {
final boolean deletedBucket = bucketDir.delete();
@@ -192,17 +192,7 @@ public class FileSystemFlowPersistenceProvider implements
FlowPersistenceProvide
protected File getSnapshotFile(final String bucketId, final String flowId,
final int version) {
final String versionExtension = version + SNAPSHOT_EXTENSION;
final Path snapshotLocation = Paths.get(getNormalizedId(bucketId),
getNormalizedId(flowId), Integer.toString(version), versionExtension);
- return getChildLocation(flowStorageDir, snapshotLocation);
- }
-
- private File getChildLocation(final File parentDir, final Path
childLocation) {
- final Path parentPath = parentDir.toPath().normalize();
- final Path childPathNormalized = childLocation.normalize();
- final Path childPath = parentPath.resolve(childPathNormalized);
- if (childPath.startsWith(parentPath)) {
- return childPath.toFile();
- }
- throw new IllegalArgumentException(String.format("Child location not
valid [%s]", childLocation));
+ return FileUtils.getChildLocation(flowStorageDir, snapshotLocation);
}
private Path getNormalizedIdPath(final String id) {
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowPersistenceProvider.java
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowPersistenceProvider.java
index d7d94814cf1..075102c0960 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowPersistenceProvider.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowPersistenceProvider.java
@@ -33,6 +33,7 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
+import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -146,10 +147,10 @@ public class GitFlowPersistenceProvider implements
MetadataAwareFlowPersistenceP
flow.putVersion(context.getVersion(), flowPointer);
- final File bucketDir = new File(flowStorageDir, bucketDirName);
- final File flowSnippetFile = new File(bucketDir, flowSnapshotFilename);
+ final File bucketDir = getChildFile(flowStorageDir, bucketDirName);
+ final File flowSnippetFile = getChildFile(bucketDir,
flowSnapshotFilename);
- final File currentBucketDir = isEmpty(currentBucketDirName) ? null :
new File(flowStorageDir, currentBucketDirName);
+ final File currentBucketDir = isEmpty(currentBucketDirName) ? null :
getChildFile(flowStorageDir, currentBucketDirName);
if (currentBucketDir != null && currentBucketDir.isDirectory()) {
if (isBucketNameChanged) {
logger.debug("Detected bucket name change from {} to {},
moving it.", currentBucketDirName, bucketDirName);
@@ -166,7 +167,7 @@ public class GitFlowPersistenceProvider implements
MetadataAwareFlowPersistenceP
try {
if (currentFlowSnapshotFilename.isPresent() &&
!flowSnapshotFilename.equals(currentFlowSnapshotFilename.get())) {
// Delete old file if flow name has been changed.
- final File latestFlowSnapshotFile = new File(bucketDir,
currentFlowSnapshotFilename.get());
+ final File latestFlowSnapshotFile = getChildFile(bucketDir,
currentFlowSnapshotFilename.get());
logger.debug("Detected flow name change from {} to {},
deleting the old snapshot file.",
currentFlowSnapshotFilename.get(),
flowSnapshotFilename);
latestFlowSnapshotFile.delete();
@@ -231,8 +232,8 @@ public class GitFlowPersistenceProvider implements
MetadataAwareFlowPersistenceP
final Flow.FlowPointer flowPointer =
flow.getFlowVersion(latestVersion);
// Delete the flow snapshot.
- final File bucketDir = new File(flowStorageDir,
bucket.getBucketDirName());
- final File flowSnapshotFile = new File(bucketDir,
flowPointer.getFileName());
+ final File bucketDir = getChildFile(flowStorageDir,
bucket.getBucketDirName());
+ final File flowSnapshotFile = getChildFile(bucketDir,
flowPointer.getFileName());
if (flowSnapshotFile.exists()) {
if (!flowSnapshotFile.delete()) {
throw new FlowPersistenceException(format("Failed to delete
flow content for %s:%s in bucket %s:%s",
@@ -264,6 +265,14 @@ public class GitFlowPersistenceProvider implements
MetadataAwareFlowPersistenceP
}
+ private File getChildFile(final File parentDir, final String childName) {
+ try {
+ return FileUtils.getChildLocation(parentDir, Paths.get(childName));
+ } catch (final IllegalArgumentException e) {
+ throw new FlowPersistenceException(e.getMessage(), e);
+ }
+ }
+
private Bucket getBucketOrFail(String bucketId) throws
FlowPersistenceException {
final Optional<Bucket> bucketOpt = flowMetaData.getBucket(bucketId);
if (!bucketOpt.isPresent()) {
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java
index 86446282158..7aa9405ed0a 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java
@@ -261,19 +261,31 @@ public class RegistryService {
}
// for each bundle in the bucket, delete all versions from the bundle
persistence provider
- for (final BundleEntity bundleEntity :
metadataService.getBundlesByBucket(existingBucket.getId())) {
+ final List<BundleEntity> bundleEntities =
metadataService.getBundlesByBucket(existingBucket.getId());
+ if (bundleEntities != null) {
+ for (final BundleEntity bundleEntity : bundleEntities) {
+ deletePersistedBundleVersions(bundleEntity);
+ }
+ }
+
+ // now delete the bucket from the metadata provider, which deletes all
flows referencing it
+ metadataService.deleteBucket(existingBucket);
+
+ return BucketMappings.map(existingBucket);
+ }
+
+ private void deletePersistedBundleVersions(final BundleEntity
bundleEntity) {
+ try {
final BundleCoordinate bundleCoordinate = new
StandardBundleCoordinate.Builder()
.bucketId(bundleEntity.getBucketId())
.groupId(bundleEntity.getGroupId())
.artifactId(bundleEntity.getArtifactId())
.build();
bundlePersistenceProvider.deleteAllBundleVersions(bundleCoordinate);
+ } catch (final IllegalArgumentException e) {
+ LOGGER.error("Unable to delete persisted content for bundle [{}]
because the stored coordinates are not a valid path",
+ bundleEntity.getId(), e);
}
-
- // now delete the bucket from the metadata provider, which deletes all
flows referencing it
- metadataService.deleteBucket(existingBucket);
-
- return BucketMappings.map(existingBucket);
}
// ---------------------- BucketItem methods
---------------------------------------------
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/StandardExtensionService.java
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/StandardExtensionService.java
index 81d6d7a7178..07431bc2df4 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/StandardExtensionService.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/StandardExtensionService.java
@@ -457,13 +457,7 @@ public class StandardExtensionService implements
ExtensionService {
metadataService.deleteBundle(bundle.getIdentifier());
// delete all content associated with the bundle in the persistence
provider
- final BundleCoordinate bundleCoordinate = new
StandardBundleCoordinate.Builder()
- .bucketId(bundle.getBucketIdentifier())
- .groupId(bundle.getGroupId())
- .artifactId(bundle.getArtifactId())
- .build();
-
- bundlePersistenceProvider.deleteAllBundleVersions(bundleCoordinate);
+ deletePersistedBundleVersions(bundle);
return bundle;
}
@@ -622,8 +616,7 @@ public class StandardExtensionService implements
ExtensionService {
metadataService.deleteBundleVersion(extensionBundleVersionId);
// delete content associated with the bundle version in the
persistence provider
- final BundleVersionCoordinate versionCoordinate =
getVersionCoordinate(bundleVersion);
- bundlePersistenceProvider.deleteBundleVersion(versionCoordinate);
+ deletePersistedBundleVersion(bundleVersion);
return bundleVersion;
}
@@ -907,6 +900,30 @@ public class StandardExtensionService implements
ExtensionService {
// ------ Helper Methods -------
+ private void deletePersistedBundleVersions(final Bundle bundle) {
+ try {
+ final BundleCoordinate bundleCoordinate = new
StandardBundleCoordinate.Builder()
+ .bucketId(bundle.getBucketIdentifier())
+ .groupId(bundle.getGroupId())
+ .artifactId(bundle.getArtifactId())
+ .build();
+
bundlePersistenceProvider.deleteAllBundleVersions(bundleCoordinate);
+ } catch (final IllegalArgumentException e) {
+ LOGGER.error("Unable to delete persisted content for bundle [{}]
because the stored coordinates are not a valid path",
+ bundle.getIdentifier(), e);
+ }
+ }
+
+ private void deletePersistedBundleVersion(final BundleVersion
bundleVersion) {
+ try {
+ final BundleVersionCoordinate versionCoordinate =
getVersionCoordinate(bundleVersion);
+ bundlePersistenceProvider.deleteBundleVersion(versionCoordinate);
+ } catch (final IllegalArgumentException e) {
+ LOGGER.error("Unable to delete persisted content for bundle
version [{}] because the stored coordinates are not a valid path",
+ bundleVersion.getVersionMetadata().getId(), e);
+ }
+ }
+
private BundleVersionCoordinate getVersionCoordinate(final BundleVersion
bundleVersion) {
return getVersionCoordinate(bundleVersion.getBundle(),
bundleVersion.getVersionMetadata());
}
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestFileSystemBundlePersistenceProvider.java
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestFileSystemBundlePersistenceProvider.java
index ce5acad2f38..df154a81b03 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestFileSystemBundlePersistenceProvider.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestFileSystemBundlePersistenceProvider.java
@@ -37,6 +37,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;
@@ -250,6 +251,52 @@ public class TestFileSystemBundlePersistenceProvider {
assertEquals(0, bundleStorageDir.listFiles().length);
}
+ @Test
+ public void testCreateRejectsParentDirectoryCoordinates() throws
IOException {
+ final File markerFile = createParentMarker();
+ try {
+ final BundleVersionCoordinate versionCoordinate =
getVersionCoordinate(BUCKET_ID, "..", "..", FIRST_VERSION,
BundleVersionType.NIFI_NAR);
+ assertThrows(IllegalArgumentException.class, () ->
createBundleVersion(fileSystemBundleProvider, versionCoordinate, "evil"));
+ assertTrue(markerFile.exists());
+ assertTrue(bundleStorageDir.exists());
+ assertFalse(new File(bundleStorageDir.getParentFile(),
FIRST_VERSION).exists());
+ } finally {
+ markerFile.delete();
+ }
+ }
+
+ @Test
+ public void testCreateAllowsSnapshotAndBuildMetadataVersions() throws
IOException {
+ final String snapshotContent = "snapshot-content";
+ final BundleVersionCoordinate snapshotCoordinate =
getVersionCoordinate(BUCKET_ID, GROUP_ID, ARTIFACT_ID, "2.0.0-SNAPSHOT",
BundleVersionType.NIFI_NAR);
+ createBundleVersion(fileSystemBundleProvider, snapshotCoordinate,
snapshotContent);
+ verifyBundleVersion(bundleStorageDir, snapshotCoordinate,
snapshotContent);
+
+ final String buildMetadataContent = "build-metadata-content";
+ final BundleVersionCoordinate buildMetadataCoordinate =
getVersionCoordinate(BUCKET_ID, GROUP_ID, ARTIFACT_ID, "1.0.0+build.5",
BundleVersionType.NIFI_NAR);
+ createBundleVersion(fileSystemBundleProvider, buildMetadataCoordinate,
buildMetadataContent);
+ verifyBundleVersion(bundleStorageDir, buildMetadataCoordinate,
buildMetadataContent);
+ }
+
+ @Test
+ public void testDeleteAllBundleVersionsRejectsParentDirectoryCoordinates()
throws IOException {
+ final File markerFile = createParentMarker();
+ try {
+ final BundleCoordinate bundleCoordinate =
getBundleCoordinate(BUCKET_ID, "..", "..");
+ assertThrows(IllegalArgumentException.class, () ->
fileSystemBundleProvider.deleteAllBundleVersions(bundleCoordinate));
+ assertTrue(markerFile.exists());
+ assertTrue(bundleStorageDir.exists());
+ } finally {
+ markerFile.delete();
+ }
+ }
+
+ private File createParentMarker() throws IOException {
+ final File markerFile = new File(bundleStorageDir.getParentFile(),
"registry-parent-marker.txt");
+ Files.writeString(markerFile.toPath(), "keep");
+ return markerFile;
+ }
+
private void createBundleVersion(final BundlePersistenceProvider
persistenceProvider,
final BundleVersionCoordinate
versionCoordinate,
final String content) throws IOException {
@@ -287,10 +334,14 @@ public class TestFileSystemBundlePersistenceProvider {
}
private static BundleCoordinate getBundleCoordinate() {
+ return getBundleCoordinate(BUCKET_ID, GROUP_ID, ARTIFACT_ID);
+ }
+
+ private static BundleCoordinate getBundleCoordinate(final String bucketId,
final String groupId, final String artifactId) {
final BundleCoordinate coordinate =
Mockito.mock(BundleCoordinate.class);
- when(coordinate.getBucketId()).thenReturn(BUCKET_ID);
- when(coordinate.getGroupId()).thenReturn(GROUP_ID);
- when(coordinate.getArtifactId()).thenReturn(ARTIFACT_ID);
+ when(coordinate.getBucketId()).thenReturn(bucketId);
+ when(coordinate.getGroupId()).thenReturn(groupId);
+ when(coordinate.getArtifactId()).thenReturn(artifactId);
return coordinate;
}
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestStandardBundleCoordinate.java
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestStandardBundleCoordinate.java
new file mode 100644
index 00000000000..d51d8b18f08
--- /dev/null
+++
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestStandardBundleCoordinate.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.
+ */
+package org.apache.nifi.registry.provider.extension;
+
+import org.apache.nifi.registry.extension.BundleCoordinate;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class TestStandardBundleCoordinate {
+
+ private static final String BUCKET_ID =
"b0000000-0000-0000-0000-000000000000";
+
+ @Test
+ void testBuildAcceptsTypicalCoordinates() {
+ final BundleCoordinate coordinate = new
StandardBundleCoordinate.Builder()
+ .bucketId(BUCKET_ID)
+ .groupId("org.apache.nifi")
+ .artifactId("nifi-standard-nar")
+ .build();
+ assertEquals(BUCKET_ID, coordinate.getBucketId());
+ assertEquals("org.apache.nifi", coordinate.getGroupId());
+ assertEquals("nifi-standard-nar", coordinate.getArtifactId());
+ }
+
+ @Test
+ void testBuildRejectsInvalidComponents() {
+ assertInvalid("..", "nifi-standard-nar");
+ assertInvalid("org.apache.nifi", "..");
+ assertInvalid(".", "nifi-standard-nar");
+ assertInvalid("org/apache", "nifi-standard-nar");
+ assertInvalid("org.apache.nifi", "art\\ifact");
+ }
+
+ private void assertInvalid(final String groupId, final String artifactId) {
+ assertThrows(IllegalArgumentException.class, () -> new
StandardBundleCoordinate.Builder()
+ .bucketId(BUCKET_ID)
+ .groupId(groupId)
+ .artifactId(artifactId)
+ .build());
+ }
+}
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestStandardBundleVersionCoordinate.java
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestStandardBundleVersionCoordinate.java
new file mode 100644
index 00000000000..a995eed2cc2
--- /dev/null
+++
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestStandardBundleVersionCoordinate.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.
+ */
+package org.apache.nifi.registry.provider.extension;
+
+import org.apache.nifi.registry.extension.BundleVersionCoordinate;
+import org.apache.nifi.registry.extension.BundleVersionType;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class TestStandardBundleVersionCoordinate {
+
+ private static final String BUCKET_ID =
"b0000000-0000-0000-0000-000000000000";
+
+ @Test
+ void testBuildAcceptsTypicalCoordinates() {
+ assertAccepted("org.apache.nifi", "nifi-standard-nar",
"2.0.0-SNAPSHOT");
+ assertAccepted("org.apache.nifi", "nifi-standard-nar",
"1.0.0+build.5");
+ }
+
+ @Test
+ void testBuildRejectsInvalidComponents() {
+ assertInvalid("..", "nifi-standard-nar", "1.0.0");
+ assertInvalid("org.apache.nifi", "..", "1.0.0");
+ assertInvalid("org.apache.nifi", "nifi-standard-nar", "..");
+ assertInvalid(".", "nifi-standard-nar", "1.0.0");
+ assertInvalid("org/apache", "nifi-standard-nar", "1.0.0");
+ }
+
+ private void assertAccepted(final String groupId, final String artifactId,
final String version) {
+ final BundleVersionCoordinate coordinate = new
StandardBundleVersionCoordinate.Builder()
+ .bucketId(BUCKET_ID)
+ .groupId(groupId)
+ .artifactId(artifactId)
+ .version(version)
+ .type(BundleVersionType.NIFI_NAR)
+ .build();
+ assertEquals(groupId, coordinate.getGroupId());
+ assertEquals(artifactId, coordinate.getArtifactId());
+ assertEquals(version, coordinate.getVersion());
+ }
+
+ private void assertInvalid(final String groupId, final String artifactId,
final String version) {
+ assertThrows(IllegalArgumentException.class, () -> new
StandardBundleVersionCoordinate.Builder()
+ .bucketId(BUCKET_ID)
+ .groupId(groupId)
+ .artifactId(artifactId)
+ .version(version)
+ .type(BundleVersionType.NIFI_NAR)
+ .build());
+ }
+}
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestFileSystemFlowPersistenceProvider.java
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestFileSystemFlowPersistenceProvider.java
index 29aa8fa6afe..f85ae596295 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestFileSystemFlowPersistenceProvider.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestFileSystemFlowPersistenceProvider.java
@@ -178,6 +178,18 @@ public class TestFileSystemFlowPersistenceProvider {
fileSystemFlowProvider.deleteFlowContent(SECOND_BUCKET_ID, FLOW_ID, 1);
}
+ @Test
+ public void testSaveRejectsParentDirectoryIdentifiers() {
+ final FlowSnapshotContext context =
Mockito.mock(FlowSnapshotContext.class);
+ when(context.getBucketId()).thenReturn("..");
+ when(context.getFlowId()).thenReturn(FLOW_ID);
+ when(context.getVersion()).thenReturn(1);
+
+ assertThrows(IllegalArgumentException.class, () ->
fileSystemFlowProvider.saveFlowContent(context,
FIRST_VERSION.getBytes(StandardCharsets.UTF_8)));
+ assertTrue(flowStorageDir.exists());
+ assertFalse(new File(flowStorageDir.getParentFile(),
FLOW_ID).exists());
+ }
+
private void createAndSaveSnapshot(final FlowPersistenceProvider
flowPersistenceProvider, final int version, final String contentString) {
final FlowSnapshotContext context =
Mockito.mock(FlowSnapshotContext.class);
when(context.getBucketId()).thenReturn(BUCKET_ID);
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/git/TestGitFlowPersistenceProvider.java
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/git/TestGitFlowPersistenceProvider.java
index c88eee6aa05..ab725af18a7 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/git/TestGitFlowPersistenceProvider.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/git/TestGitFlowPersistenceProvider.java
@@ -42,6 +42,9 @@ import java.util.function.Consumer;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class TestGitFlowPersistenceProvider {
@@ -289,6 +292,32 @@ public class TestGitFlowPersistenceProvider {
}, true);
}
+ @Test
+ public void testSaveRejectsParentDirectoryBucketName() throws
GitAPIException, IOException {
+ final Map<String, String> properties = new HashMap<>();
+ properties.put(GitFlowPersistenceProvider.FLOW_STORAGE_DIR_PROP,
"target/git-parent-dir-bucket");
+
+ assertProvider(properties, g -> { }, p -> {
+ final StandardFlowSnapshotContext context = new
StandardFlowSnapshotContext.Builder()
+ .bucketId("bucket-id-A")
+ .bucketName("..")
+ .flowId("flow-id-1")
+ .flowName("flow")
+ .author("unit-test-user")
+ .comments("Initial commit.")
+ .snapshotTimestamp(new Date().getTime())
+ .version(1)
+ .build();
+
+ assertThrows(FlowPersistenceException.class, () ->
p.saveFlowContent(context, "content".getBytes(StandardCharsets.UTF_8)));
+
+ final File gitDir = new File("target/git-parent-dir-bucket");
+ assertTrue(gitDir.exists());
+ final File escapedSnapshot = new File(gitDir.getParentFile(),
"flow.snapshot");
+ assertFalse(escapedSnapshot.exists());
+ }, true);
+ }
+
@Test
public void testLoadLargeFlow() throws GitAPIException, IOException {
final Map<String, String> properties = new HashMap<>();
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java
index dd9df4bb739..2851a382597 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java
@@ -24,6 +24,7 @@ import org.apache.nifi.flow.VersionedProcessGroup;
import org.apache.nifi.flow.VersionedProcessor;
import org.apache.nifi.registry.bucket.Bucket;
import org.apache.nifi.registry.db.entity.BucketEntity;
+import org.apache.nifi.registry.db.entity.BundleEntity;
import org.apache.nifi.registry.db.entity.FlowEntity;
import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
import org.apache.nifi.registry.diff.ComponentDifference;
@@ -65,6 +66,7 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -295,6 +297,31 @@ public class TestRegistryService {
.deleteAllFlowContent(eq(bucketToDelete.getId()),
eq(flowToDelete.getId()));
}
+ @Test
+ public void testDeleteBucketWithInvalidBundleCoordinates() {
+ final BucketEntity bucketToDelete = new BucketEntity();
+ bucketToDelete.setId("b1");
+ bucketToDelete.setName("My Bucket");
+ bucketToDelete.setCreated(new Date());
+
+ final BundleEntity unsafeBundle = new BundleEntity();
+ unsafeBundle.setId("bundle1");
+ unsafeBundle.setBucketId(bucketToDelete.getId());
+ unsafeBundle.setGroupId("..");
+ unsafeBundle.setArtifactId("..");
+
+
when(metadataService.getBucketById(bucketToDelete.getId())).thenReturn(bucketToDelete);
+
when(metadataService.getFlowsByBucket(bucketToDelete.getId())).thenReturn(Collections.emptyList());
+
when(metadataService.getBundlesByBucket(bucketToDelete.getId())).thenReturn(Collections.singletonList(unsafeBundle));
+
+ final Bucket deletedBucket =
registryService.deleteBucket(bucketToDelete.getId());
+ assertNotNull(deletedBucket);
+ assertEquals(bucketToDelete.getId(), deletedBucket.getIdentifier());
+
+ verify(metadataService).deleteBucket(bucketToDelete);
+ verify(bundlePersistenceProvider,
never()).deleteAllBundleVersions(any());
+ }
+
// ---------------------- Test VersionedFlow methods
---------------------------------------------
@Test
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FileUtils.java
b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FileUtils.java
index c2f5c8eb1b4..f848e050307 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FileUtils.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FileUtils.java
@@ -414,4 +414,31 @@ public class FileUtils {
}
return cleanName.toString();
}
+
+ /**
+ * Resolves {@code childLocation} against {@code parentDir} and returns
the resulting file only when
+ * the normalized absolute path remains a strict child of the parent.
Relative parent segments,
+ * absolute child locations, and paths that resolve to the parent itself
are rejected.
+ *
+ * @param parentDir the directory that must contain the result
+ * @param childLocation a relative path to resolve under the parent
+ * @return the resolved child file
+ */
+ public static File getChildLocation(final File parentDir, final Path
childLocation) {
+ if (parentDir == null) {
+ throw new IllegalArgumentException("Parent directory is required");
+ }
+
+ if (childLocation == null || childLocation.isAbsolute()) {
+ throw new IllegalArgumentException(String.format("Child location
not valid [%s]", childLocation));
+ }
+
+ final Path parentPath =
parentDir.toPath().toAbsolutePath().normalize();
+ final Path childPath = parentPath.resolve(childLocation).normalize();
+ if (!childPath.startsWith(parentPath) || childPath.equals(parentPath))
{
+ throw new IllegalArgumentException(String.format("Child location
not valid [%s]", childLocation));
+ }
+
+ return childPath.toFile();
+ }
}
diff --git
a/nifi-registry/nifi-registry-core/nifi-registry-utils/src/test/java/org/apache/nifi/registry/util/TestFileUtils.java
b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/test/java/org/apache/nifi/registry/util/TestFileUtils.java
index 5679f5cefb6..268e20e23a1 100644
---
a/nifi-registry/nifi-registry-core/nifi-registry-utils/src/test/java/org/apache/nifi/registry/util/TestFileUtils.java
+++
b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/test/java/org/apache/nifi/registry/util/TestFileUtils.java
@@ -18,8 +18,15 @@
package org.apache.nifi.registry.util;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
public class TestFileUtils {
@Test
@@ -28,4 +35,27 @@ public class TestFileUtils {
final String sanitizedFilename = FileUtils.sanitizeFilename(filename);
assertEquals("This___is___a_test", sanitizedFilename);
}
+
+ @Test
+ public void testGetChildLocationAcceptsContainedPath(@TempDir final Path
tempDir) {
+ final File parentDir = tempDir.toFile();
+ final File child = FileUtils.getChildLocation(parentDir,
Paths.get("bucket", "group", "artifact"));
+ final Path parentPath =
parentDir.toPath().toAbsolutePath().normalize();
+ final Path childPath = child.toPath().toAbsolutePath().normalize();
+ assertTrue(childPath.startsWith(parentPath));
+ assertEquals(parentPath.resolve(Paths.get("bucket", "group",
"artifact")), childPath);
+ }
+
+ @Test
+ public void testGetChildLocationRejectsEscapeAndIdentity(@TempDir final
Path tempDir) {
+ final File parentDir = tempDir.toFile();
+ assertThrows(IllegalArgumentException.class, () ->
FileUtils.getChildLocation(parentDir, Paths.get("..")));
+ assertThrows(IllegalArgumentException.class, () ->
FileUtils.getChildLocation(parentDir, Paths.get("..", "1.0.0")));
+ assertThrows(IllegalArgumentException.class, () ->
FileUtils.getChildLocation(parentDir, Paths.get("..", "..")));
+ assertThrows(IllegalArgumentException.class, () ->
FileUtils.getChildLocation(parentDir, Paths.get(".")));
+ assertThrows(IllegalArgumentException.class, () ->
FileUtils.getChildLocation(parentDir, Paths.get("")));
+ assertThrows(IllegalArgumentException.class, () ->
FileUtils.getChildLocation(parentDir, tempDir.resolve("other")));
+ assertThrows(IllegalArgumentException.class, () ->
FileUtils.getChildLocation(null, Paths.get("child")));
+ assertThrows(IllegalArgumentException.class, () ->
FileUtils.getChildLocation(parentDir, null));
+ }
}
diff --git
a/nifi-registry/nifi-registry-extensions/nifi-registry-aws/nifi-registry-aws-extensions/src/main/java/org/apache/nifi/registry/aws/S3BundlePersistenceProvider.java
b/nifi-registry/nifi-registry-extensions/nifi-registry-aws/nifi-registry-aws-extensions/src/main/java/org/apache/nifi/registry/aws/S3BundlePersistenceProvider.java
index 776bfa6efbc..0a0717afde9 100644
---
a/nifi-registry/nifi-registry-extensions/nifi-registry-aws/nifi-registry-aws-extensions/src/main/java/org/apache/nifi/registry/aws/S3BundlePersistenceProvider.java
+++
b/nifi-registry/nifi-registry-extensions/nifi-registry-aws/nifi-registry-aws-extensions/src/main/java/org/apache/nifi/registry/aws/S3BundlePersistenceProvider.java
@@ -331,7 +331,12 @@ public class S3BundlePersistenceProvider implements
BundlePersistenceProvider {
}
private static String sanitize(final String input) {
- return FileUtils.sanitizeFilename(input).trim().toLowerCase();
+ final String sanitized =
FileUtils.sanitizeFilename(input).trim().toLowerCase();
+ if (".".equals(sanitized) || "..".equals(sanitized)) {
+ throw new IllegalArgumentException("Coordinate component is not a
valid path name");
+ }
+
+ return sanitized;
}
static String getBundleFileExtension(final BundleVersionType bundleType) {