Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: fine-grained permissions #173

Merged
merged 1 commit into from
Jul 31, 2024
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
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