Skip to content

Commit 015f7d8

Browse files
committed
ice: Fix localfileio file:/// & --force-no-copy handling
1 parent 5c8b297 commit 015f7d8

File tree

3 files changed

+106
-34
lines changed

3 files changed

+106
-34
lines changed

ice-rest-catalog/src/main/java/com/altinity/ice/rest/catalog/internal/config/Config.java

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import java.io.File;
2323
import java.io.IOException;
2424
import java.net.URI;
25-
import java.nio.file.Paths;
2625
import java.util.Arrays;
2726
import java.util.HashMap;
2827
import java.util.Map;
@@ -284,14 +283,8 @@ public void putNotNullOrEmpty(String key, String value) {
284283
}
285284

286285
if (warehouse.startsWith("file://")) {
287-
if (!m.containsKey(LocalFileIO.LOCALFILEIO_PROP_BASEDIR)) {
288-
// FIXME: wrong thing to do if warehouse is absolute
289-
m.put(LocalFileIO.LOCALFILEIO_PROP_BASEDIR, new File(".").getAbsolutePath());
290-
}
291-
var warehouseLocation =
292-
Strings.removePrefix(m.get(CatalogProperties.WAREHOUSE_LOCATION), "file://");
293-
File d = Paths.get(m.get(LocalFileIO.LOCALFILEIO_PROP_BASEDIR), warehouseLocation).toFile();
294-
;
286+
String workdir = LocalFileIO.resolveWorkdir(warehouse, localFileIOBaseDir);
287+
File d = LocalFileIO.resolveWarehousePath(warehouse, workdir).toFile();
295288
// TODO: move it out of here; toIcebergConfig() should not have side-effects
296289
if (!d.isDirectory() && !d.mkdirs()) {
297290
throw new IOException("Unable to create " + d);

ice/src/main/java/com/altinity/ice/cli/internal/config/Config.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ public record Config(
3131
@JsonPropertyDescription(
3232
"/path/to/dir where to store downloaded files when `ice insert`ing from http:// & https://")
3333
String httpCacheDir,
34+
@JsonPropertyDescription("Path to warehouse, e.g. s3://..., file://...") String warehouse,
3435
@JsonPropertyDescription("/path/to/dir to serve as a root for warehouse=file://...")
3536
String localFileIOBaseDir,
37+
@JsonPropertyDescription("Locations outside the warehouse that ice is allowed to access")
38+
String[] localFileIOAllowAccess,
3639
@JsonPropertyDescription("Settings for warehouse=s3://...") S3 s3,
3740
@JsonPropertyDescription(
3841
"(experimental) Iceberg properties (see https://iceberg.apache.org/javadoc/1.8.1/"
@@ -102,7 +105,13 @@ public void putNotNullOrEmpty(String key, String value) {
102105
}
103106
}
104107

108+
m.putNotNullOrEmpty(LocalFileIO.LOCALFILEIO_PROP_WAREHOUSE, warehouse);
105109
m.putNotNullOrEmpty(LocalFileIO.LOCALFILEIO_PROP_BASEDIR, localFileIOBaseDir);
110+
if (localFileIOAllowAccess != null) {
111+
m.putNotNullOrEmpty(
112+
LocalFileIO.LOCALFILEIO_PROP_ALLOWACCESS, String.join(",", localFileIOAllowAccess));
113+
}
114+
106115
m.putNotNullOrEmpty(OPTION_HTTP_CACHE, httpCacheDir);
107116

108117
if (icebergProperties != null) {

ice/src/main/java/com/altinity/ice/internal/iceberg/io/LocalFileIO.java

Lines changed: 95 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616
import java.nio.file.Path;
1717
import java.nio.file.Paths;
1818
import java.util.ArrayList;
19+
import java.util.Arrays;
1920
import java.util.Comparator;
2021
import java.util.List;
2122
import java.util.Map;
23+
import java.util.Objects;
2224
import java.util.stream.Collectors;
2325
import java.util.stream.Stream;
26+
import javax.annotation.Nullable;
2427
import org.apache.iceberg.CatalogProperties;
2528
import org.apache.iceberg.exceptions.RuntimeIOException;
2629
import org.apache.iceberg.io.BulkDeletionFailureException;
@@ -32,12 +35,15 @@
3235

3336
public class LocalFileIO implements DelegateFileIO {
3437

35-
// current workdir by default
38+
public static final String LOCALFILEIO_PROP_WAREHOUSE = "localfileio.warehouse";
39+
3640
public static final String LOCALFILEIO_PROP_BASEDIR = "localfileio.basedir";
37-
public static final String LOCALFILEIO_PROP_WAREHOUSE =
38-
"localfileio.warehouse"; // e.g. file://path/to/warehouse/root/relative/to/localfileio.basedir
39-
private Path basePath;
41+
public static final String LOCALFILEIO_PROP_ALLOWACCESS = "localfileio.allowaccess";
42+
43+
private String workDir;
44+
private Path warehousePath;
4045
private String locationPrefix;
46+
private List<Path> allowAccess = List.of(); // TODO: replace with trie
4147

4248
@Override
4349
public void initialize(Map<String, String> properties) {
@@ -51,40 +57,104 @@ public void initialize(Map<String, String> properties) {
5157
warehouse.startsWith("file://"),
5258
"\"%s\" must start with file://",
5359
LOCALFILEIO_PROP_WAREHOUSE);
54-
String baseDir = properties.getOrDefault(LOCALFILEIO_PROP_BASEDIR, "");
55-
if (baseDir.isEmpty()) {
56-
baseDir = System.getProperty("user.dir");
57-
}
58-
if (!baseDir.isEmpty()) {
59-
baseDir = Strings.removeSuffix(baseDir, "/") + "/";
60-
}
61-
String base = baseDir + Strings.removeSuffix(Strings.removePrefix(warehouse, "file://"), "/");
62-
if (!Files.isDirectory(Paths.get(base))) {
60+
this.workDir = resolveWorkdir(warehouse, properties.get(LOCALFILEIO_PROP_BASEDIR));
61+
Path warehousePath = resolveWarehousePath(warehouse, workDir);
62+
if (!Files.isDirectory(warehousePath)) {
6363
throw new IllegalArgumentException(
64-
String.format("\"%s\" must point to an existing directory", LOCALFILEIO_PROP_WAREHOUSE));
64+
String.format("\"%s\" must point to an existing directory", warehousePath));
6565
}
66-
Path basePath;
6766
try {
68-
basePath = Paths.get(base).toRealPath();
67+
this.warehousePath = warehousePath.toRealPath();
6968
} catch (IOException e) {
7069
throw new RuntimeIOException(e);
7170
}
72-
// TODO: do no allow dir that contain subfolders other than data/metadata (unless force flag is
73-
// used)
71+
var x = Strings.removeSuffix(Strings.removePrefix(warehouse, "file://"), "/");
72+
this.locationPrefix = "file://" + (!x.isEmpty() ? x + "/" : "");
73+
this.allowAccess =
74+
Arrays.stream(properties.getOrDefault(LOCALFILEIO_PROP_ALLOWACCESS, "").split(","))
75+
.map(s -> Strings.removePrefix(s.trim(), "file://"))
76+
.filter(s -> !s.isEmpty())
77+
.map(Paths::get)
78+
.toList();
79+
}
80+
81+
/**
82+
* resolveWorkdir returns workdir based on whether warehouse is absolute or relative, e.g. <code>
83+
* resolveWorkdir("/foo/bar", null) -> "/"
84+
* resolveWorkdir("foo/bar", "/") -> "/"
85+
* resolveWorkdir("foo/bar", "/baz") -> "/baz"
86+
* resolveWorkdir("foo/bar", null) -> "$PWD"
87+
* </code>
88+
*/
89+
public static String resolveWorkdir(String fileWarehouse, @Nullable String warehouseBaseDir) {
90+
String baseDir = Objects.requireNonNullElse(warehouseBaseDir, "");
91+
if (baseDir.isEmpty()) {
92+
baseDir =
93+
Strings.removePrefix(fileWarehouse, "file://").startsWith("/")
94+
? "/"
95+
: System.getProperty("user.dir") /* workdir */;
96+
}
97+
return baseDir;
98+
}
99+
100+
/**
101+
* resolveBasePath returns warehouse's absolute path, e.g. <code>
102+
* resolveBasePath("/foo/bar", "/") -> "/foo/bar"
103+
* resolveBasePath("foo/bar", "/") -> "/foo/bar"
104+
* resolveBasePath("foo/bar", "/baz") -> "/baz/foo/bar"
105+
* resolveBasePath("foo/bar", "$PWD") -> "$PWD/foo/bar"
106+
* </code>
107+
*/
108+
public static Path resolveWarehousePath(String fileWarehouse, String workDir) {
109+
Path basePath =
110+
Paths.get(
111+
Strings.removeSuffix(workDir, "/")
112+
+ "/"
113+
+ Strings.removeSuffix(Strings.removePrefix(fileWarehouse, "file://"), "/"))
114+
.toAbsolutePath();
115+
// TODO: do no allow dir that contain subfolders other than data/metadata
116+
// (unless force flag is used)
74117
if ("/".equals(basePath.toString())) {
75118
throw new IllegalArgumentException(
76119
String.format("\"%s\" cannot point to /", LOCALFILEIO_PROP_WAREHOUSE));
77120
}
78-
this.basePath = basePath;
79-
var x = Strings.removeSuffix(Strings.removePrefix(warehouse, "file://"), "/");
80-
this.locationPrefix = "file://" + (!x.isEmpty() ? x + "/" : "");
121+
return basePath;
81122
}
82123

124+
// TODO: refactor (resolve + all resolve* above); this feels way more complicated that it needs to
125+
// be
83126
private Path resolve(String userPath) {
84127
String relativeToBase = Strings.removePrefix(userPath, this.locationPrefix);
85-
Path resolved = basePath.resolve(relativeToBase).normalize().toAbsolutePath();
86-
if (!resolved.startsWith(basePath)) {
87-
throw new SecurityException(String.format("Access outside \"%s\" is not allowed", resolved));
128+
if (relativeToBase.startsWith("file://")) {
129+
// userPath is not relative to locationPrefix (expected when --force-no-copy is used)
130+
// check if it's explicitly whitelisted by the user
131+
relativeToBase = Strings.removePrefix(relativeToBase, "file://");
132+
if (allowAccess.isEmpty()) {
133+
throw new SecurityException(
134+
String.format(
135+
"Access outside \"%s\" is not allowed: \"%s\" (add `localFileIOAllowAccess: [\"/dir\"]` to .ice.yaml to whitelist)",
136+
warehousePath, relativeToBase));
137+
}
138+
Path resolved;
139+
if (relativeToBase.startsWith("/")) {
140+
resolved = Paths.get(relativeToBase).toAbsolutePath();
141+
} else {
142+
resolved = Paths.get(workDir).resolve(relativeToBase).toAbsolutePath();
143+
}
144+
if (allowAccess.stream().noneMatch(resolved::startsWith)) {
145+
throw new SecurityException(
146+
String.format(
147+
"Access outside \"%s\" (plus \"%s\") is not allowed: \"%s\"",
148+
warehousePath,
149+
String.join("\", \"", allowAccess.stream().map(Path::toString).toList()),
150+
relativeToBase));
151+
}
152+
return resolved;
153+
}
154+
Path resolved = warehousePath.resolve(relativeToBase).normalize().toAbsolutePath();
155+
if (!resolved.startsWith(warehousePath)) {
156+
throw new SecurityException(
157+
String.format("Access outside \"%s\" is not allowed: \"%s\"", warehousePath, resolved));
88158
}
89159
return resolved;
90160
}
@@ -107,7 +177,7 @@ public void deleteFile(String path) {
107177
}
108178

109179
private String location(Path path) {
110-
return locationPrefix + basePath.relativize(path);
180+
return locationPrefix + warehousePath.relativize(path);
111181
}
112182

113183
@Override

0 commit comments

Comments
 (0)