webdav-server/lib/permissions.go
2024-07-31 11:06:34 +02:00

131 lines
2.5 KiB
Go

package lib
import (
"errors"
"fmt"
"net/http"
"path/filepath"
"regexp"
"strings"
)
type Rule struct {
Permissions Permissions
Path string
Regex *regexp.Regexp
}
func (r *Rule) Validate() error {
if r.Regex != nil && r.Path != "" {
return errors.New("invalid rule: cannot define both regex and path")
}
return nil
}
// Matches checks if [Rule] matches the given path.
func (r *Rule) Matches(path string) bool {
if r.Regex != nil {
return r.Regex.MatchString(path)
}
return strings.HasPrefix(path, r.Path)
}
type UserPermissions struct {
Directory string
Permissions Permissions
Rules []*Rule
}
// Allowed checks if the user has permission to access a directory/file
func (p UserPermissions) Allowed(r *http.Request, destinationExists func(string) bool) bool {
// Go through rules beginning from the last one.
for i := len(p.Rules) - 1; i >= 0; i-- {
rule := p.Rules[i]
if rule.Matches(r.URL.Path) {
return rule.Permissions.Allowed(r, destinationExists)
}
}
return p.Permissions.Allowed(r, destinationExists)
}
func (p *UserPermissions) Validate() error {
var err error
p.Directory, err = filepath.Abs(p.Directory)
if err != nil {
return fmt.Errorf("invalid permissions: %w", err)
}
for _, r := range p.Rules {
if err := r.Validate(); err != nil {
return fmt.Errorf("invalid permissions: %w", err)
}
}
return nil
}
type Permissions struct {
Create bool
Read bool
Update bool
Delete bool
}
func (p *Permissions) UnmarshalText(data []byte) error {
text := strings.ToLower(string(data))
if text == "none" {
return nil
}
for _, c := range text {
switch c {
case 'c':
p.Create = true
case 'r':
p.Read = true
case 'u':
p.Update = true
case 'd':
p.Delete = true
default:
return fmt.Errorf("invalid permission: %q", c)
}
}
return nil
}
func (p Permissions) Allowed(r *http.Request, destinationExists func(string) bool) bool {
switch r.Method {
case "GET", "HEAD", "OPTIONS", "POST", "PROPFIND":
// Note: POST backend implementation just returns the same thing as GET.
return p.Read
case "MKCOL":
return p.Create
case "PROPPATCH":
return p.Update
case "PUT":
if destinationExists(r.URL.Path) {
return p.Update
} else {
return p.Create
}
case "COPY", "MOVE":
if destinationExists(r.Header.Get("Destination")) {
return p.Update
} else {
return p.Create
}
case "DELETE":
return p.Delete
case "LOCK", "UNLOCK":
return p.Create || p.Read || p.Update || p.Delete
default:
return false
}
}