feat: add defaultDenyRead mode for strict filesystem isolation (#24)
This commit is contained in:
@@ -37,10 +37,12 @@ type NetworkConfig struct {
|
||||
|
||||
// FilesystemConfig defines filesystem restrictions.
|
||||
type FilesystemConfig struct {
|
||||
DenyRead []string `json:"denyRead"`
|
||||
AllowWrite []string `json:"allowWrite"`
|
||||
DenyWrite []string `json:"denyWrite"`
|
||||
AllowGitConfig bool `json:"allowGitConfig,omitempty"`
|
||||
DefaultDenyRead bool `json:"defaultDenyRead,omitempty"` // If true, deny reads by default except system paths and AllowRead
|
||||
AllowRead []string `json:"allowRead"` // Paths to allow reading (used when DefaultDenyRead is true)
|
||||
DenyRead []string `json:"denyRead"`
|
||||
AllowWrite []string `json:"allowWrite"`
|
||||
DenyWrite []string `json:"denyWrite"`
|
||||
AllowGitConfig bool `json:"allowGitConfig,omitempty"`
|
||||
}
|
||||
|
||||
// CommandConfig defines command restrictions.
|
||||
@@ -179,6 +181,9 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if slices.Contains(c.Filesystem.AllowRead, "") {
|
||||
return errors.New("filesystem.allowRead contains empty path")
|
||||
}
|
||||
if slices.Contains(c.Filesystem.DenyRead, "") {
|
||||
return errors.New("filesystem.denyRead contains empty path")
|
||||
}
|
||||
@@ -427,7 +432,11 @@ func Merge(base, override *Config) *Config {
|
||||
},
|
||||
|
||||
Filesystem: FilesystemConfig{
|
||||
// Boolean fields: true if either enables it
|
||||
DefaultDenyRead: base.Filesystem.DefaultDenyRead || override.Filesystem.DefaultDenyRead,
|
||||
|
||||
// Append slices
|
||||
AllowRead: mergeStrings(base.Filesystem.AllowRead, override.Filesystem.AllowRead),
|
||||
DenyRead: mergeStrings(base.Filesystem.DenyRead, override.Filesystem.DenyRead),
|
||||
AllowWrite: mergeStrings(base.Filesystem.AllowWrite, override.Filesystem.AllowWrite),
|
||||
DenyWrite: mergeStrings(base.Filesystem.DenyWrite, override.Filesystem.DenyWrite),
|
||||
|
||||
@@ -111,6 +111,15 @@ func TestConfigValidate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty allowRead path",
|
||||
config: Config{
|
||||
Filesystem: FilesystemConfig{
|
||||
AllowRead: []string{""},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty denyRead path",
|
||||
config: Config{
|
||||
@@ -453,6 +462,50 @@ func TestMerge(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("merge defaultDenyRead and allowRead", func(t *testing.T) {
|
||||
base := &Config{
|
||||
Filesystem: FilesystemConfig{
|
||||
DefaultDenyRead: true,
|
||||
AllowRead: []string{"/home/user/project"},
|
||||
},
|
||||
}
|
||||
override := &Config{
|
||||
Filesystem: FilesystemConfig{
|
||||
AllowRead: []string{"/home/user/other"},
|
||||
},
|
||||
}
|
||||
result := Merge(base, override)
|
||||
|
||||
if !result.Filesystem.DefaultDenyRead {
|
||||
t.Error("expected DefaultDenyRead to be true (from base)")
|
||||
}
|
||||
if len(result.Filesystem.AllowRead) != 2 {
|
||||
t.Errorf("expected 2 allowRead paths, got %d: %v", len(result.Filesystem.AllowRead), result.Filesystem.AllowRead)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("merge defaultDenyRead from override", func(t *testing.T) {
|
||||
base := &Config{
|
||||
Filesystem: FilesystemConfig{
|
||||
DefaultDenyRead: false,
|
||||
},
|
||||
}
|
||||
override := &Config{
|
||||
Filesystem: FilesystemConfig{
|
||||
DefaultDenyRead: true,
|
||||
AllowRead: []string{"/home/user/project"},
|
||||
},
|
||||
}
|
||||
result := Merge(base, override)
|
||||
|
||||
if !result.Filesystem.DefaultDenyRead {
|
||||
t.Error("expected DefaultDenyRead to be true (from override)")
|
||||
}
|
||||
if len(result.Filesystem.AllowRead) != 1 {
|
||||
t.Errorf("expected 1 allowRead path, got %d", len(result.Filesystem.AllowRead))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("override ports", func(t *testing.T) {
|
||||
base := &Config{
|
||||
Network: NetworkConfig{
|
||||
|
||||
Reference in New Issue
Block a user