Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/en/deployment/hadoop_java_sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ Please refer to the following table to set the relevant parameters of the JuiceF
| `juicefs.backup-skip-trash` | `false` | Skip files and directories in trash when backup metadata. |
| `juicefs.heartbeat` | 12 | Heartbeat interval (in seconds) between client and metadata engine. It's recommended that all clients use the same value. |
| `juicefs.skip-dir-mtime` | 100ms | Minimal duration to modify parent dir mtime. |
| `juicefs.subdir` | | Allow access only to the subpaths of this directory. all other paths, including the root or sibling directories, will be denied access. |
| `juicefs.subdir` | | Allow access only to the subpaths of this directory. Multiple paths can be specified, separated by commas. All other paths, including the root or sibling directories, will be denied access. |

#### Multiple file systems configuration

Expand Down
2 changes: 1 addition & 1 deletion docs/zh_cn/deployment/hadoop_java_sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ make win
|`juicefs.backup-skip-trash`| `false` | 备份元数据时忽略回收站中的文件和目录。 |
| `juicefs.heartbeat` | 12 | 客户端和元数据引擎之间的心跳间隔(单位:秒),建议所有客户端都设置一样 |
| `juicefs.skip-dir-mtime` | 100ms | 修改父目录 mtime 间隔。 |
| `juicefs.subdir` | | 仅允许访问此目录的子路径。 |
| `juicefs.subdir` | | 仅允许访问此目录的子路径。可以指定多个路径,使用逗号分隔。所有其他路径,包括根目录或同级目录,都将被拒绝访问。 |

#### 多文件系统配置

Expand Down
39 changes: 36 additions & 3 deletions pkg/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ type FileSystem struct {
opsDurationsHistogram prometheus.Histogram

registry *prometheus.Registry

// Pre-parsed subdir prefixes for fast path checking
subdirPrefixes []string
}

type File struct {
Expand Down Expand Up @@ -205,6 +208,18 @@ func NewFileSystem(conf *vfs.Config, m meta.Meta, d chunk.ChunkStore, registry *
registry: registry,
}

// Pre-parse subdir prefixes for fast path checking
if conf.Subdir != "" {
subdirs := strings.Split(conf.Subdir, ",")
fs.subdirPrefixes = make([]string, 0, len(subdirs))
for _, prefix := range subdirs {
prefix = strings.TrimSpace(prefix)
if prefix != "" {
fs.subdirPrefixes = append(fs.subdirPrefixes, prefix)
}
}
}

go fs.cleanupCache()
if conf.AccessLog != "" {
f, err := os.OpenFile(conf.AccessLog, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
Expand Down Expand Up @@ -859,10 +874,28 @@ func (fs *FileSystem) resolve(ctx meta.Context, p string, followLastSymlink bool
}

func (fs *FileSystem) doResolve(ctx meta.Context, p string, followLastSymlink bool, visited map[Ino]struct{}) (fi *FileStat, err syscall.Errno) {
prefix := fs.conf.Subdir
p = path.Clean(p)
if !strings.HasPrefix(p, prefix) || len(prefix) > 0 && len(p) > len(prefix) && p[len(prefix)] != '/' {
return nil, syscall.EACCES

// Check if path is allowed by any of the configured subdirs
if len(fs.subdirPrefixes) > 0 {
allowed := false
plen := len(p)
for _, prefix := range fs.subdirPrefixes {
prefixLen := len(prefix)
// Fast path: check length first to avoid string comparison if possible
if prefixLen > plen {
continue
}
// Check if path starts with prefix and is either the prefix itself or has '/' after prefix
// This prevents matching "/test" with "/testfile" (should match "/test" or "/test/...")
if strings.HasPrefix(p, prefix) && (prefixLen == plen || p[prefixLen] == '/') {
allowed = true
break
}
}
if !allowed {
return nil, syscall.EACCES
}
}
var inode Ino
var attr = &Attr{}
Expand Down
27 changes: 20 additions & 7 deletions sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -441,14 +441,27 @@ public void initialize(URI uri, Configuration conf) throws IOException {
obj.put(key, Boolean.valueOf(getConf(conf, key, "false")));
}
String subdir = getConf(conf, "subdir", "");
if (subdir.equals("/")) {
subdir = "";
} else if (!subdir.startsWith("/")) {
subdir = "/" + subdir;
}
subdir = subdir.replaceAll("/+$", "");
if (!subdir.isEmpty()) {
LOG.debug("subdir {} is enabled", subdir);
// Support multiple subdirs separated by comma
String[] subdirs = subdir.split(",");
List<String> normalizedSubdirs = new ArrayList<>();
for (String sd : subdirs) {
sd = sd.trim();
if (sd.isEmpty() || sd.equals("/")) {
continue; // skip empty string or root
}
if (!sd.startsWith("/")) {
sd = "/" + sd;
}
sd = sd.replaceAll("/+$", "");
normalizedSubdirs.add(sd);
}
if (normalizedSubdirs.isEmpty()) {
subdir = "";
} else {
subdir = String.join(",", normalizedSubdirs);
LOG.debug("subdir {} is enabled", subdir);
}
}
obj.put("bucket", getConf(conf, "bucket", ""));
obj.put("storageClass", getConf(conf, "storage-class", ""));
Expand Down
81 changes: 81 additions & 0 deletions sdk/java/src/test/java/io/juicefs/JuiceFileSystemTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1253,4 +1253,85 @@ public void testSubdir() throws IOException, InterruptedException {
}
newFS.close();
}

public void testMultipleSubdirs() throws IOException, InterruptedException {
Configuration newConf = new Configuration(cfg);
newConf.set("fs.defaultFS", "jfs://test/");
newConf.set("juicefs.name", "test");
newConf.set("juicefs.test.meta", newConf.get("juicefs.dev.meta"));

// Create multiple subdirs
Path subdir1 = new Path("/subdir1");
Path subdir2 = new Path("/subdir2");
Path subdir3 = new Path("/subdir3");

fs.delete(subdir1, true);
fs.delete(subdir2, true);
fs.delete(subdir3, true);

fs.mkdirs(subdir1);
fs.mkdirs(subdir2);
fs.mkdirs(subdir3);
fs.setPermission(subdir1, new FsPermission((short) 0777));
fs.setPermission(subdir2, new FsPermission((short) 0777));
fs.setPermission(subdir3, new FsPermission((short) 0777));

// Set multiple subdirs separated by comma
newConf.set("juicefs.subdir", "/subdir1,/subdir2,/subdir3");
FileSystem newFS = FileSystem.newInstance(newConf);

// Test file operations within subdir1
assertTrue(newFS.mkdirs(new Path("/subdir1/dir1")));
newFS.create(new Path("/subdir1/dir1/f1")).close();
assertTrue(newFS.exists(new Path("/subdir1/dir1/f1")));

// Test file operations within subdir2
assertTrue(newFS.mkdirs(new Path("/subdir2/dir2")));
newFS.create(new Path("/subdir2/dir2/f2")).close();
assertTrue(newFS.exists(new Path("/subdir2/dir2/f2")));

// Test file operations within subdir3
assertTrue(newFS.mkdirs(new Path("/subdir3/dir3")));
newFS.create(new Path("/subdir3/dir3/f3")).close();
assertTrue(newFS.exists(new Path("/subdir3/dir3/f3")));

// Test file operations not within any subdir
Path nonexistent = new Path("/nonexistent");
try {
newFS.exists(nonexistent);
fail("exists should not work because the path is not under any subdir");
} catch (AccessControlException e) {
assertTrue(e.getMessage().contains("Permission denied"));
}
try {
newFS.mkdirs(nonexistent);
fail("mkdirs should not work because the path is not under any subdir");
} catch (AccessControlException e) {
assertTrue(e.getMessage().contains("Permission denied"));
}
try {
newFS.create(nonexistent);
fail("create should not work because the path is not under any subdir");
} catch (AccessControlException e) {
assertTrue(e.getMessage().contains("Permission denied"));
}

// Test creating a path with the same prefix but not under any subdir
Path wrongPathWithSamePrefix = new Path("/subdir1_wrong");
fs.mkdirs(wrongPathWithSamePrefix);
try {
newFS.listStatus(wrongPathWithSamePrefix);
fail("listStatus should not work because the path is not under any subdir");
} catch (AccessControlException e) {
assertTrue(e.getMessage().contains("Permission denied"));
}

// Test that paths in different subdirs are accessible
assertTrue(newFS.exists(new Path("/subdir1/dir1/f1")));
assertTrue(newFS.exists(new Path("/subdir2/dir2/f2")));
assertTrue(newFS.exists(new Path("/subdir3/dir3/f3")));

// Cleanup
newFS.close();
}
}
Loading