167 lines
3.7 KiB
Go
167 lines
3.7 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, fileExists func(string) bool) bool {
|
|
// For COPY and MOVE requests, we first check the permissions for the destination
|
|
// path. As soon as a rule matches and does not allow the operation at the destination,
|
|
// we fail immediately. If no rule matches, we check the global permissions.
|
|
if r.Method == "COPY" || r.Method == "MOVE" {
|
|
dst := r.Header.Get("Destination")
|
|
|
|
for i := len(p.Rules) - 1; i >= 0; i-- {
|
|
if p.Rules[i].Matches(dst) {
|
|
if !p.Rules[i].Permissions.AllowedDestination(r, fileExists) {
|
|
return false
|
|
}
|
|
|
|
// Only check the first rule that matches, similarly to the source rules.
|
|
break
|
|
}
|
|
}
|
|
|
|
if !p.Permissions.AllowedDestination(r, fileExists) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Go through rules beginning from the last one, and check the permissions at
|
|
// the source. The first matched rule returns.
|
|
for i := len(p.Rules) - 1; i >= 0; i-- {
|
|
if p.Rules[i].Matches(r.URL.Path) {
|
|
return p.Rules[i].Permissions.Allowed(r, fileExists)
|
|
}
|
|
}
|
|
|
|
return p.Permissions.Allowed(r, fileExists)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Allowed returns whether this permission set has permissions to execute this
|
|
// request in the source directory. This applies to all requests with all methods.
|
|
func (p Permissions) Allowed(r *http.Request, fileExists 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 fileExists(r.URL.Path) {
|
|
return p.Update
|
|
} else {
|
|
return p.Create
|
|
}
|
|
case "COPY":
|
|
return p.Read
|
|
case "MOVE":
|
|
return p.Read && p.Delete
|
|
case "DELETE":
|
|
return p.Delete
|
|
case "LOCK", "UNLOCK":
|
|
return p.Create || p.Read || p.Update || p.Delete
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// AllowedDestination returns whether this permissions set has permissions to execute this
|
|
// request in the destination directory. This only applies for COPY and MOVE requests.
|
|
func (p Permissions) AllowedDestination(r *http.Request, fileExists func(string) bool) bool {
|
|
switch r.Method {
|
|
case "COPY", "MOVE":
|
|
if fileExists(r.Header.Get("Destination")) {
|
|
return p.Update
|
|
} else {
|
|
return p.Create
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
}
|