Skip to content

Commit

Permalink
feat!: fine-grained permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed Jul 31, 2024
1 parent f4de82c commit b5a3d07
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 87 deletions.
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,10 @@ debug: false
# Default is '.' (current directory).
directory: .

# The default modification permissions for users. Default is 'false'.
modify: true
# The default permissions for users. This is a case insensitive option. Possible
# permissions: C (Create), R (Read), U (Update), D (Delete). You can combine multiple
# permissions. For example, to allow to read and create, set "RC". Default is "R".
permissions: R

# The default permissions rules for users. Default is none.
rules: []
Expand Down Expand Up @@ -120,19 +122,19 @@ users:
password: "{env}ENV_PASSWORD"
- username: basic
password: basic
# Override default modify.
modify: false
# Override default permissions.
permissions: CRUD
rules:
# With this rule, the user CANNOT access /some/files.
- path: /some/file
allow: false
# With this rule, the user CAN modify /public/access.
permissions: none
# With this rule, the user CAN create, read, update and delete within /public/access.
- path: /public/access/
modify: true
# With this rule, the user CAN modify all files ending with .js. It uses
permissions: CRUD
# With this rule, the user CAN read and update all files ending with .js. It uses
# a regular expression.
- regex: "^.+.js$"
modify: true
permissions: RU
```
### CORS
Expand Down
32 changes: 16 additions & 16 deletions lib/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,18 @@ const (
)

type Config struct {
Permissions `mapstructure:",squash"`
Debug bool
Address string
Port int
TLS bool
Cert string
Key string
Prefix string
NoSniff bool
Log Log
CORS CORS
Users []User
UserPermissions `mapstructure:",squash"`
Debug bool
Address string
Port int
TLS bool
Cert string
Key string
Prefix string
NoSniff bool
Log Log
CORS CORS
Users []User
}

func ParseConfig(filename string, flags *pflag.FlagSet) (*Config, error) {
Expand Down Expand Up @@ -74,7 +74,7 @@ func ParseConfig(filename string, flags *pflag.FlagSet) (*Config, error) {

// Other defaults
v.SetDefault("Directory", ".")
v.SetDefault("Modify", false)
v.SetDefault("Permissions", "R")
v.SetDefault("Debug", false)
v.SetDefault("NoSniff", false)
v.SetDefault("Log.Format", "console")
Expand Down Expand Up @@ -108,8 +108,8 @@ func ParseConfig(filename string, flags *pflag.FlagSet) (*Config, error) {
cfg.Users[i].Directory = cfg.Directory
}

if !v.IsSet(fmt.Sprintf("Users.%d.Modify", i)) {
cfg.Users[i].Modify = cfg.Modify
if !v.IsSet(fmt.Sprintf("Users.%d.Permissions", i)) {
cfg.Users[i].Permissions = cfg.Permissions
}

if !v.IsSet(fmt.Sprintf("Users.%d.Rules", i)) {
Expand Down Expand Up @@ -153,7 +153,7 @@ func (c *Config) Validate() error {
}
}

err = c.Permissions.Validate()
err = c.UserPermissions.Validate()
if err != nil {
return fmt.Errorf("invalid config: %w", err)
}
Expand Down
48 changes: 28 additions & 20 deletions lib/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,36 +49,44 @@ func TestConfigCascade(t *testing.T) {
t.Parallel()

check := func(t *testing.T, cfg *Config) {
require.True(t, cfg.Modify)
require.True(t, cfg.Permissions.Read)
require.True(t, cfg.Permissions.Create)
require.False(t, cfg.Permissions.Delete)
require.False(t, cfg.Permissions.Update)
require.Equal(t, "/", cfg.Directory)
require.Len(t, cfg.Rules, 1)

require.Len(t, cfg.Users, 2)

require.True(t, cfg.Users[0].Modify)
require.True(t, cfg.Users[0].Permissions.Read)
require.True(t, cfg.Users[0].Permissions.Create)
require.False(t, cfg.Users[0].Permissions.Delete)
require.False(t, cfg.Users[0].Permissions.Update)
require.Equal(t, "/", cfg.Users[0].Directory)
require.Len(t, cfg.Users[0].Rules, 1)

require.False(t, cfg.Users[1].Modify)
require.True(t, cfg.Users[1].Permissions.Read)
require.False(t, cfg.Users[1].Permissions.Create)
require.False(t, cfg.Users[1].Permissions.Delete)
require.False(t, cfg.Users[1].Permissions.Update)
require.Equal(t, "/basic", cfg.Users[1].Directory)
require.Len(t, cfg.Users[1].Rules, 0)
}

t.Run("YAML", func(t *testing.T) {
content := `
directory: /
modify: true
permissions: CR
rules:
- path: /public/access/
modify: true
permissions: R
users:
- username: admin
password: admin
- username: basic
password: basic
directory: /basic
modify: false
permissions: R
rules: []`

cfg := writeAndParseConfig(t, content, ".yml")
Expand All @@ -90,11 +98,11 @@ users:
t.Run("JSON", func(t *testing.T) {
content := `{
"directory": "/",
"modify": true,
"permissions": "CR",
"rules": [
{
"path": "/public/access/",
"modify": true
"permissions": "R"
}
],
"users": [
Expand All @@ -106,7 +114,7 @@ users:
"username": "basic",
"password": "basic",
"directory": "/basic",
"modify": false,
"permissions": "R",
"rules": []
}
]
Expand All @@ -121,11 +129,11 @@ users:
t.Run("`TOML", func(t *testing.T) {
content := `
directory = "/"
modify = true
permissions = "CR"
[[rules]]
path = "/public/access/"
modify = true
permissions = "R"
[[users]]
username = "admin"
Expand All @@ -135,7 +143,7 @@ password = "admin"
username = "basic"
password = "basic"
directory = "/basic"
modify = false
permissions = "R"
rules = []
`

Expand Down Expand Up @@ -175,12 +183,9 @@ cors:
func TestConfigRules(t *testing.T) {
content := `
directory: /
modify: true
rules:
- regex: '^.+\.js$'
modify: true
- path: /public/access/
modify: true`
- path: /public/access/`

cfg := writeAndParseConfig(t, content, ".yaml")
require.NoError(t, cfg.Validate())
Expand All @@ -199,7 +204,7 @@ rules:
func TestConfigEnv(t *testing.T) {
require.NoError(t, os.Setenv("WD_PORT", "1234"))
require.NoError(t, os.Setenv("WD_DEBUG", "true"))
require.NoError(t, os.Setenv("WD_MODIFY", "true"))
require.NoError(t, os.Setenv("WD_PERMISSIONS", "CRUD"))
require.NoError(t, os.Setenv("WD_DIRECTORY", "/test"))

cfg, err := ParseConfig("", nil)
Expand All @@ -208,11 +213,14 @@ func TestConfigEnv(t *testing.T) {
assert.Equal(t, 1234, cfg.Port)
assert.Equal(t, "/test", cfg.Directory)
assert.Equal(t, true, cfg.Debug)
assert.Equal(t, true, cfg.Modify)
require.True(t, cfg.Permissions.Read)
require.True(t, cfg.Permissions.Create)
require.True(t, cfg.Permissions.Delete)
require.True(t, cfg.Permissions.Update)

// Reset
require.NoError(t, os.Setenv("WD_PORT", ""))
require.NoError(t, os.Setenv("WD_DEBUG", ""))
require.NoError(t, os.Setenv("WD_MODIFY", ""))
require.NoError(t, os.Setenv("WD_PERMISSIONS", ""))
require.NoError(t, os.Setenv("WD_DIRECTORY", ""))
}
14 changes: 12 additions & 2 deletions lib/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package lib

import (
"net/http"
"net/url"
"os"
"strings"

"github.com/rs/cors"
Expand All @@ -23,7 +25,7 @@ func NewHandler(c *Config) (http.Handler, error) {
h := &Handler{
user: &handlerUser{
User: User{
Permissions: c.Permissions,
UserPermissions: c.UserPermissions,
},
Handler: webdav.Handler{
Prefix: c.Prefix,
Expand Down Expand Up @@ -100,7 +102,15 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

// Checks for user permissions relatively to this PATH.
allowed := user.Allowed(r)
allowed := user.Allowed(r, func(destination string) bool {
u, err := url.Parse(destination)
if err != nil {
return false
}
path := strings.TrimPrefix(u.Path, user.Prefix)
_, err = user.FileSystem.Stat(r.Context(), path)
return !os.IsNotExist(err)
})

zap.L().Debug("allowed & method & path", zap.Bool("allowed", allowed), zap.String("method", r.Method), zap.String("path", r.URL.Path))

Expand Down
41 changes: 33 additions & 8 deletions lib/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func TestServerAuthentication(t *testing.T) {

srv := makeTestServer(t, fmt.Sprintf(`
directory: %s
modify: true
permissions: CRUD
users:
- username: basic
Expand Down Expand Up @@ -191,39 +191,58 @@ func TestServerRules(t *testing.T) {
"a/foo.js": []byte("foo js"),
"a/foo.txt": []byte("foo txt"),
"b/foo.txt": []byte("foo b"),
"c/a.txt": []byte("b"),
"c/b.txt": []byte("b"),
"c/c.txt": []byte("b"),
})

srv := makeTestServer(t, fmt.Sprintf(`
directory: %s
modify: true
permissions: CRUD
users:
- username: basic
password: basic
rules:
- regex: "^.+.js$"
modify: false
permissions: R
- path: "/b"
modify: false
permissions: R
- path: "/a/foo.txt"
permissions: none
- path: "/c"
permissions: none
`, dir))

client := gowebdav.NewClient(srv.URL, "basic", "basic")

files, err := client.ReadDir("/")
require.NoError(t, err)
require.Len(t, files, 3)
require.Len(t, files, 4)

err = client.Write("/foo.txt", []byte("new"), 0666)
require.NoError(t, err)

err = client.Write("/a/foo.txt", []byte("new"), 0666)
err = client.Write("/new.txt", []byte("new"), 0666)
require.NoError(t, err)

_, err = client.Read("/a/foo.txt")
require.ErrorContains(t, err, "403")

err = client.Write("/a/foo.js", []byte("new"), 0666)
require.ErrorContains(t, err, "403")

err = client.Write("/b/foo.txt", []byte("new"), 0666)
require.ErrorContains(t, err, "403")

_, err = client.ReadDir("/c")
require.ErrorContains(t, err, "403")

_, err = client.Read("/c/a.txt")
require.ErrorContains(t, err, "403")

err = client.Write("/c/b.txt", []byte("new"), 0666)
require.ErrorContains(t, err, "403")
}

func TestServerPermissions(t *testing.T) {
Expand All @@ -237,7 +256,7 @@ func TestServerPermissions(t *testing.T) {

srv := makeTestServer(t, fmt.Sprintf(`
directory: %s
modify: true
permissions: CR
users:
- username: a
Expand All @@ -246,7 +265,7 @@ users:
- username: b
password: b
directory: %s/b
modify: false
permissions: R
`, dir, dir, dir))

t.Run("User A", func(t *testing.T) {
Expand All @@ -265,6 +284,12 @@ users:
err = client.Copy("/foo.txt", "/copy.txt", false)
require.NoError(t, err)

err = client.Copy("/foo.txt", "/copy.txt", true)
require.ErrorContains(t, err, "403")

err = client.Rename("/foo.txt", "/copy.txt", true)
require.ErrorContains(t, err, "403")

data, err = client.Read("/copy.txt")
require.NoError(t, err)
require.EqualValues(t, []byte("foo a"), data)
Expand Down
Loading

0 comments on commit b5a3d07

Please sign in to comment.