1616import java .nio .file .Path ;
1717import java .nio .file .Paths ;
1818import java .util .ArrayList ;
19+ import java .util .Arrays ;
1920import java .util .Comparator ;
2021import java .util .List ;
2122import java .util .Map ;
23+ import java .util .Objects ;
2224import java .util .stream .Collectors ;
2325import java .util .stream .Stream ;
26+ import javax .annotation .Nullable ;
2427import org .apache .iceberg .CatalogProperties ;
2528import org .apache .iceberg .exceptions .RuntimeIOException ;
2629import org .apache .iceberg .io .BulkDeletionFailureException ;
3235
3336public 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