Compare commits
10 Commits
172968d292
...
main
Author | SHA1 | Date | |
---|---|---|---|
173b2cc388 | |||
|
1d5a5dcd6c | ||
|
9c900b1c66 | ||
|
e4babc2798 | ||
|
87062437b6 | ||
|
75c185517e | ||
|
b689d5daff | ||
|
002c347f47 | ||
|
1b10baf554 | ||
|
aba953c3b6 |
@@ -4,6 +4,12 @@ packages:
|
|||||||
sources:
|
sources:
|
||||||
- https://github.com/emersion/go-webdav
|
- https://github.com/emersion/go-webdav
|
||||||
tasks:
|
tasks:
|
||||||
|
- build: |
|
||||||
|
cd go-webdav
|
||||||
|
go build -race -v ./...
|
||||||
- test: |
|
- test: |
|
||||||
cd go-webdav
|
cd go-webdav
|
||||||
go test -v ./...
|
go test -race -v ./...
|
||||||
|
- gofmt: |
|
||||||
|
cd go-webdav
|
||||||
|
test -z $(gofmt -l .)
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
# go-webdav
|
# go-webdav
|
||||||
|
Fork of go-webdav that supports ACL, synced with [original repo](https://github.com/emersion/go-webdav) (Implementation of ACL from [oliverpool/go-webdav](https://github.com/oliverpool/go-webdav))
|
||||||
|
|
||||||
|
|
||||||
[](https://pkg.go.dev/github.com/emersion/go-webdav)
|
[](https://pkg.go.dev/github.com/emersion/go-webdav)
|
||||||
|
|
||||||
@@ -9,5 +11,5 @@ A Go library for [WebDAV], [CalDAV] and [CardDAV].
|
|||||||
MIT
|
MIT
|
||||||
|
|
||||||
[WebDAV]: https://tools.ietf.org/html/rfc4918
|
[WebDAV]: https://tools.ietf.org/html/rfc4918
|
||||||
[CalDAV]: https://tools.ietf.org/html/rfc4791
|
[CalDAV]: https://tools.ietf.org/html/rfc4791
|
||||||
[CardDAV]: https://tools.ietf.org/html/rfc6352
|
[CardDAV]: https://tools.ietf.org/html/rfc6352
|
||||||
|
@@ -79,6 +79,12 @@ type CalendarCompRequest struct {
|
|||||||
|
|
||||||
AllComps bool
|
AllComps bool
|
||||||
Comps []CalendarCompRequest
|
Comps []CalendarCompRequest
|
||||||
|
|
||||||
|
Expand *CalendarExpandRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
type CalendarExpandRequest struct {
|
||||||
|
Start, End time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompFilter struct {
|
type CompFilter struct {
|
||||||
|
@@ -154,7 +154,9 @@ func encodeCalendarReq(c *CalendarCompRequest) (*internal.Prop, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
calDataReq := calendarDataReq{Comp: compReq}
|
expandReq := encodeExpandRequest(c.Expand)
|
||||||
|
|
||||||
|
calDataReq := calendarDataReq{Comp: compReq, Expand: expandReq}
|
||||||
|
|
||||||
getLastModReq := internal.NewRawXMLElement(internal.GetLastModifiedName, nil, nil)
|
getLastModReq := internal.NewRawXMLElement(internal.GetLastModifiedName, nil, nil)
|
||||||
getETagReq := internal.NewRawXMLElement(internal.GetETagName, nil, nil)
|
getETagReq := internal.NewRawXMLElement(internal.GetETagName, nil, nil)
|
||||||
@@ -172,6 +174,55 @@ func encodeCompFilter(filter *CompFilter) *compFilter {
|
|||||||
for _, child := range filter.Comps {
|
for _, child := range filter.Comps {
|
||||||
encoded.CompFilters = append(encoded.CompFilters, *encodeCompFilter(&child))
|
encoded.CompFilters = append(encoded.CompFilters, *encodeCompFilter(&child))
|
||||||
}
|
}
|
||||||
|
for _, pf := range filter.Props {
|
||||||
|
encoded.PropFilters = append(encoded.PropFilters, *encodePropFilter(&pf))
|
||||||
|
}
|
||||||
|
return &encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodePropFilter(filter *PropFilter) *propFilter {
|
||||||
|
encoded := propFilter{Name: filter.Name}
|
||||||
|
if !filter.Start.IsZero() || !filter.End.IsZero() {
|
||||||
|
encoded.TimeRange = &timeRange{
|
||||||
|
Start: dateWithUTCTime(filter.Start),
|
||||||
|
End: dateWithUTCTime(filter.End),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encoded.TextMatch = encodeTextMatch(filter.TextMatch)
|
||||||
|
for _, pf := range filter.ParamFilter {
|
||||||
|
encoded.ParamFilter = append(encoded.ParamFilter, encodeParamFilter(pf))
|
||||||
|
}
|
||||||
|
return &encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeParamFilter(pf ParamFilter) paramFilter {
|
||||||
|
encoded := paramFilter{
|
||||||
|
Name: pf.Name,
|
||||||
|
TextMatch: encodeTextMatch(pf.TextMatch),
|
||||||
|
}
|
||||||
|
return encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeTextMatch(tm *TextMatch) *textMatch {
|
||||||
|
if tm == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded := &textMatch{
|
||||||
|
Text: tm.Text,
|
||||||
|
NegateCondition: negateCondition(tm.NegateCondition),
|
||||||
|
}
|
||||||
|
return encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeExpandRequest(e *CalendarExpandRequest) *expand {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
encoded := expand{
|
||||||
|
Start: dateWithUTCTime(e.Start),
|
||||||
|
End: dateWithUTCTime(e.End),
|
||||||
|
}
|
||||||
return &encoded
|
return &encoded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -179,7 +179,8 @@ func (t *dateWithUTCTime) MarshalText() ([]byte, error) {
|
|||||||
type calendarDataReq struct {
|
type calendarDataReq struct {
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav calendar-data"`
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav calendar-data"`
|
||||||
Comp *comp `xml:"comp,omitempty"`
|
Comp *comp `xml:"comp,omitempty"`
|
||||||
// TODO: expand, limit-recurrence-set, limit-freebusy-set
|
Expand *expand `xml:"expand,omitempty"`
|
||||||
|
// TODO: limit-recurrence-set, limit-freebusy-set
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc4791#section-9.6.1
|
// https://tools.ietf.org/html/rfc4791#section-9.6.1
|
||||||
@@ -194,6 +195,12 @@ type comp struct {
|
|||||||
Comp []comp `xml:"comp,omitempty"`
|
Comp []comp `xml:"comp,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type expand struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav expand"`
|
||||||
|
Start dateWithUTCTime `xml:"start,attr"`
|
||||||
|
End dateWithUTCTime `xml:"end,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc4791#section-9.6.4
|
// https://tools.ietf.org/html/rfc4791#section-9.6.4
|
||||||
type prop struct {
|
type prop struct {
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav prop"`
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav prop"`
|
||||||
|
@@ -366,7 +366,7 @@ func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth internal.Depth) (*internal.MultiStatus, error) {
|
func (b *backend) PropFind(w http.ResponseWriter, r *http.Request, propfind *internal.PropFind, depth internal.Depth) error {
|
||||||
resType := b.resourceTypeAtPath(r.URL.Path)
|
resType := b.resourceTypeAtPath(r.URL.Path)
|
||||||
|
|
||||||
var dataReq CalendarCompRequest
|
var dataReq CalendarCompRequest
|
||||||
@@ -376,86 +376,92 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i
|
|||||||
case resourceTypeRoot:
|
case resourceTypeRoot:
|
||||||
resp, err := b.propFindRoot(r.Context(), propfind)
|
resp, err := b.propFindRoot(r.Context(), propfind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
case resourceTypeUserPrincipal:
|
case resourceTypeUserPrincipal:
|
||||||
principalPath, err := b.Backend.CurrentUserPrincipal(r.Context())
|
principalPath, err := b.Backend.CurrentUserPrincipal(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if r.URL.Path == principalPath {
|
if r.URL.Path == principalPath {
|
||||||
resp, err := b.propFindUserPrincipal(r.Context(), propfind)
|
resp, err := b.propFindUserPrincipal(r.Context(), propfind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
if depth != internal.DepthZero {
|
if depth != internal.DepthZero {
|
||||||
resp, err := b.propFindHomeSet(r.Context(), propfind)
|
resp, err := b.propFindHomeSet(r.Context(), propfind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
if depth == internal.DepthInfinity {
|
if depth == internal.DepthInfinity {
|
||||||
resps_, err := b.propFindAllCalendars(r.Context(), propfind, true)
|
resps_, err := b.propFindAllCalendars(r.Context(), propfind, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, resps_...)
|
resps = append(resps, resps_...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
http.Redirect(w, r, principalPath, http.StatusPermanentRedirect) // keep http method
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
case resourceTypeCalendarHomeSet:
|
case resourceTypeCalendarHomeSet:
|
||||||
homeSetPath, err := b.Backend.CalendarHomeSetPath(r.Context())
|
homeSetPath, err := b.Backend.CalendarHomeSetPath(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if r.URL.Path == homeSetPath {
|
if r.URL.Path == homeSetPath {
|
||||||
resp, err := b.propFindHomeSet(r.Context(), propfind)
|
resp, err := b.propFindHomeSet(r.Context(), propfind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
if depth != internal.DepthZero {
|
if depth != internal.DepthZero {
|
||||||
recurse := depth == internal.DepthInfinity
|
recurse := depth == internal.DepthInfinity
|
||||||
resps_, err := b.propFindAllCalendars(r.Context(), propfind, recurse)
|
resps_, err := b.propFindAllCalendars(r.Context(), propfind, recurse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, resps_...)
|
resps = append(resps, resps_...)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
http.Redirect(w, r, homeSetPath, http.StatusPermanentRedirect) // keep http method
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
case resourceTypeCalendar:
|
case resourceTypeCalendar:
|
||||||
ab, err := b.Backend.GetCalendar(r.Context(), r.URL.Path)
|
ab, err := b.Backend.GetCalendar(r.Context(), r.URL.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resp, err := b.propFindCalendar(r.Context(), propfind, ab)
|
resp, err := b.propFindCalendar(r.Context(), propfind, ab)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
if depth != internal.DepthZero {
|
if depth != internal.DepthZero {
|
||||||
resps_, err := b.propFindAllCalendarObjects(r.Context(), propfind, ab)
|
resps_, err := b.propFindAllCalendarObjects(r.Context(), propfind, ab)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, resps_...)
|
resps = append(resps, resps_...)
|
||||||
}
|
}
|
||||||
case resourceTypeCalendarObject:
|
case resourceTypeCalendarObject:
|
||||||
ao, err := b.Backend.GetCalendarObject(r.Context(), r.URL.Path, &dataReq)
|
ao, err := b.Backend.GetCalendarObject(r.Context(), r.URL.Path, &dataReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := b.propFindCalendarObject(r.Context(), propfind, ao)
|
resp, err := b.propFindCalendarObject(r.Context(), propfind, ao)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
return internal.NewMultiStatus(resps...), nil
|
return internal.ServeMultiStatus(w, internal.NewMultiStatus(resps...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) propFindRoot(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
|
func (b *backend) propFindRoot(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
|
||||||
|
@@ -233,3 +233,35 @@ func (t testBackend) ListCalendarObjects(ctx context.Context, path string, req *
|
|||||||
func (t testBackend) QueryCalendarObjects(ctx context.Context, path string, query *CalendarQuery) ([]CalendarObject, error) {
|
func (t testBackend) QueryCalendarObjects(ctx context.Context, path string, query *CalendarQuery) ([]CalendarObject, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRedirections(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
calendars := []Calendar{{Path: "/user/calendars/a"}}
|
||||||
|
ts := httptest.NewServer(&Handler{&testBackend{
|
||||||
|
calendars: calendars,
|
||||||
|
}, ""})
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
client, err := NewClient(nil, ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error creating client: %s", err)
|
||||||
|
}
|
||||||
|
hsp, err := client.FindCalendarHomeSet(ctx, "/must-be-redirected/")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error finding home set path: %s", err)
|
||||||
|
}
|
||||||
|
if want := "/user/calendars/"; hsp != want {
|
||||||
|
t.Fatalf("Found home set path '%s', expected '%s'", hsp, want)
|
||||||
|
}
|
||||||
|
abs, err := client.FindCalendars(ctx, "/must-be-redirected/again/")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error finding calendars: %s", err)
|
||||||
|
}
|
||||||
|
if len(abs) != 1 {
|
||||||
|
t.Fatalf("Found %d calendars, expected 1", len(abs))
|
||||||
|
}
|
||||||
|
if want := "/user/calendars/a"; abs[0].Path != want {
|
||||||
|
t.Fatalf("Found calendar at %s, expected %s", abs[0].Path, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -28,6 +28,7 @@ type AddressBook struct {
|
|||||||
Description string
|
Description string
|
||||||
MaxResourceSize int64
|
MaxResourceSize int64
|
||||||
SupportedAddressData []AddressDataType
|
SupportedAddressData []AddressDataType
|
||||||
|
ReadOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ab *AddressBook) SupportsAddressData(contentType, version string) bool {
|
func (ab *AddressBook) SupportsAddressData(contentType, version string) bool {
|
||||||
@@ -107,6 +108,7 @@ type AddressObject struct {
|
|||||||
ContentLength int64
|
ContentLength int64
|
||||||
ETag string
|
ETag string
|
||||||
Card vcard.Card
|
Card vcard.Card
|
||||||
|
ReadOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyncQuery is the query struct represents a sync-collection request
|
// SyncQuery is the query struct represents a sync-collection request
|
||||||
|
@@ -194,6 +194,43 @@ func TestAddressBookDiscovery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRedirections(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
h := Handler{&testBackend{}, ""}
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
ctx = context.WithValue(ctx, currentUserPrincipalKey, "/principal/")
|
||||||
|
ctx = context.WithValue(ctx, homeSetPathKey, "/principal/contacts/")
|
||||||
|
ctx = context.WithValue(ctx, addressBookPathKey, "/principal/contacts/default")
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
(&h).ServeHTTP(w, r)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
client, err := NewClient(nil, ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error creating client: %s", err)
|
||||||
|
}
|
||||||
|
hsp, err := client.FindAddressBookHomeSet(ctx, "/must-be-redirected/")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error finding home set path: %s", err)
|
||||||
|
}
|
||||||
|
if want := "/principal/contacts/"; hsp != want {
|
||||||
|
t.Fatalf("Found home set path '%s', expected '%s'", hsp, want)
|
||||||
|
}
|
||||||
|
abs, err := client.FindAddressBooks(ctx, "/must-be-redirected/again/")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error finding address books: %s", err)
|
||||||
|
}
|
||||||
|
if len(abs) != 1 {
|
||||||
|
t.Fatalf("Found %d address books, expected 1", len(abs))
|
||||||
|
}
|
||||||
|
if want := "/principal/contacts/default"; abs[0].Path != want {
|
||||||
|
t.Fatalf("Found address book at %s, expected %s", abs[0].Path, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var mkcolRequestBody = `
|
var mkcolRequestBody = `
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<D:mkcol xmlns:D="DAV:"
|
<D:mkcol xmlns:D="DAV:"
|
||||||
|
@@ -332,7 +332,7 @@ func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth internal.Depth) (*internal.MultiStatus, error) {
|
func (b *backend) PropFind(w http.ResponseWriter, r *http.Request, propfind *internal.PropFind, depth internal.Depth) error {
|
||||||
resType := b.resourceTypeAtPath(r.URL.Path)
|
resType := b.resourceTypeAtPath(r.URL.Path)
|
||||||
|
|
||||||
var dataReq AddressDataRequest
|
var dataReq AddressDataRequest
|
||||||
@@ -342,86 +342,92 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i
|
|||||||
case resourceTypeRoot:
|
case resourceTypeRoot:
|
||||||
resp, err := b.propFindRoot(r.Context(), propfind)
|
resp, err := b.propFindRoot(r.Context(), propfind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
case resourceTypeUserPrincipal:
|
case resourceTypeUserPrincipal:
|
||||||
principalPath, err := b.Backend.CurrentUserPrincipal(r.Context())
|
principalPath, err := b.Backend.CurrentUserPrincipal(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if r.URL.Path == principalPath {
|
if r.URL.Path == principalPath {
|
||||||
resp, err := b.propFindUserPrincipal(r.Context(), propfind)
|
resp, err := b.propFindUserPrincipal(r.Context(), propfind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
if depth != internal.DepthZero {
|
if depth != internal.DepthZero {
|
||||||
resp, err := b.propFindHomeSet(r.Context(), propfind)
|
resp, err := b.propFindHomeSet(r.Context(), propfind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
if depth == internal.DepthInfinity {
|
if depth == internal.DepthInfinity {
|
||||||
resps_, err := b.propFindAllAddressBooks(r.Context(), propfind, true)
|
resps_, err := b.propFindAllAddressBooks(r.Context(), propfind, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, resps_...)
|
resps = append(resps, resps_...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
http.Redirect(w, r, principalPath, http.StatusPermanentRedirect) // keep http method
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
case resourceTypeAddressBookHomeSet:
|
case resourceTypeAddressBookHomeSet:
|
||||||
homeSetPath, err := b.Backend.AddressBookHomeSetPath(r.Context())
|
homeSetPath, err := b.Backend.AddressBookHomeSetPath(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if r.URL.Path == homeSetPath {
|
if r.URL.Path == homeSetPath {
|
||||||
resp, err := b.propFindHomeSet(r.Context(), propfind)
|
resp, err := b.propFindHomeSet(r.Context(), propfind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
if depth != internal.DepthZero {
|
if depth != internal.DepthZero {
|
||||||
recurse := depth == internal.DepthInfinity
|
recurse := depth == internal.DepthInfinity
|
||||||
resps_, err := b.propFindAllAddressBooks(r.Context(), propfind, recurse)
|
resps_, err := b.propFindAllAddressBooks(r.Context(), propfind, recurse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, resps_...)
|
resps = append(resps, resps_...)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
http.Redirect(w, r, homeSetPath, http.StatusPermanentRedirect) // keep http method
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
case resourceTypeAddressBook:
|
case resourceTypeAddressBook:
|
||||||
ab, err := b.Backend.GetAddressBook(r.Context(), r.URL.Path)
|
ab, err := b.Backend.GetAddressBook(r.Context(), r.URL.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resp, err := b.propFindAddressBook(r.Context(), propfind, ab)
|
resp, err := b.propFindAddressBook(r.Context(), propfind, ab)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
if depth != internal.DepthZero {
|
if depth != internal.DepthZero {
|
||||||
resps_, err := b.propFindAllAddressObjects(r.Context(), propfind, ab)
|
resps_, err := b.propFindAllAddressObjects(r.Context(), propfind, ab)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, resps_...)
|
resps = append(resps, resps_...)
|
||||||
}
|
}
|
||||||
case resourceTypeAddressObject:
|
case resourceTypeAddressObject:
|
||||||
ao, err := b.Backend.GetAddressObject(r.Context(), r.URL.Path, &dataReq)
|
ao, err := b.Backend.GetAddressObject(r.Context(), r.URL.Path, &dataReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := b.propFindAddressObject(r.Context(), propfind, ao)
|
resp, err := b.propFindAddressObject(r.Context(), propfind, ao)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps = append(resps, *resp)
|
resps = append(resps, *resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
return internal.NewMultiStatus(resps...), nil
|
return internal.ServeMultiStatus(w, internal.NewMultiStatus(resps...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) propFindRoot(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
|
func (b *backend) propFindRoot(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
|
||||||
@@ -490,13 +496,23 @@ func (b *backend) propFindAddressBook(ctx context.Context, propfind *internal.Pr
|
|||||||
}
|
}
|
||||||
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil
|
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil
|
||||||
},
|
},
|
||||||
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName, addressBookName)),
|
internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||||
supportedAddressDataName: internal.PropFindValue(&supportedAddressData{
|
return internal.NewResourceType(internal.CollectionName, addressBookName), nil
|
||||||
Types: []addressDataType{
|
},
|
||||||
{ContentType: vcard.MIMEType, Version: "3.0"},
|
supportedAddressDataName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||||
{ContentType: vcard.MIMEType, Version: "4.0"},
|
return &supportedAddressData{
|
||||||
},
|
Types: []addressDataType{
|
||||||
}),
|
{ContentType: vcard.MIMEType, Version: "3.0"},
|
||||||
|
{ContentType: vcard.MIMEType, Version: "4.0"},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
internal.CurrentUserPrivilegeSetName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||||
|
if ab.ReadOnly {
|
||||||
|
return &internal.CurrentUserPrivilegeSetReadOnly, nil
|
||||||
|
}
|
||||||
|
return &internal.CurrentUserPrivilegeSetReadWrite, nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if ab.Name != "" {
|
if ab.Name != "" {
|
||||||
@@ -563,6 +579,12 @@ func (b *backend) propFindAddressObject(ctx context.Context, propfind *internal.
|
|||||||
|
|
||||||
return &addressDataResp{Data: buf.Bytes()}, nil
|
return &addressDataResp{Data: buf.Bytes()}, nil
|
||||||
},
|
},
|
||||||
|
internal.CurrentUserPrivilegeSetName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||||
|
if ao.ReadOnly {
|
||||||
|
return &internal.CurrentUserPrivilegeSetReadOnly, nil
|
||||||
|
}
|
||||||
|
return &internal.CurrentUserPrivilegeSetReadWrite, nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if ao.ContentLength > 0 {
|
if ao.ContentLength > 0 {
|
||||||
|
19
fs_local.go
19
fs_local.go
@@ -2,8 +2,10 @@ package webdav
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -64,11 +66,17 @@ func fileInfoFromOS(p string, fi os.FileInfo) *FileInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func errFromOS(err error) error {
|
func errFromOS(err error) error {
|
||||||
if os.IsNotExist(err) {
|
// Remove path from path errors so it's not returned to the user
|
||||||
|
var perr *fs.PathError
|
||||||
|
if errors.As(err, &perr) {
|
||||||
|
err = fmt.Errorf("%s: %w", perr.Op, perr.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
return NewHTTPError(http.StatusNotFound, err)
|
return NewHTTPError(http.StatusNotFound, err)
|
||||||
} else if os.IsPermission(err) {
|
} else if errors.Is(err, fs.ErrPermission) {
|
||||||
return NewHTTPError(http.StatusForbidden, err)
|
return NewHTTPError(http.StatusForbidden, err)
|
||||||
} else if os.IsTimeout(err) {
|
} else if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||||
return NewHTTPError(http.StatusServiceUnavailable, err)
|
return NewHTTPError(http.StatusServiceUnavailable, err)
|
||||||
} else {
|
} else {
|
||||||
return err
|
return err
|
||||||
@@ -95,9 +103,12 @@ func (fs LocalFileSystem) ReadDir(ctx context.Context, name string, recursive bo
|
|||||||
|
|
||||||
var l []FileInfo
|
var l []FileInfo
|
||||||
err = filepath.Walk(path, func(p string, fi os.FileInfo, err error) error {
|
err = filepath.Walk(path, func(p string, fi os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil && !errors.Is(err, os.ErrPermission) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if fi == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
href, err := fs.externalPath(p)
|
href, err := fs.externalPath(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
548
internal/acl.go
Normal file
548
internal/acl.go
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewPrivilege(name xml.Name) Privilege {
|
||||||
|
return Privilege{
|
||||||
|
Raw: NewRawXMLElement(name, nil, nil),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Privilege struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: privilege"`
|
||||||
|
Raw *RawXMLValue `xml:",any"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Privilege) Is(target xml.Name) bool {
|
||||||
|
got, ok := p.Raw.XMLName()
|
||||||
|
return ok && got == target
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
/*
|
||||||
|
rfc3744#section-3.1
|
||||||
|
|
||||||
|
The read privilege controls methods that return information about the
|
||||||
|
state of the resource, including the resource's properties. Affected
|
||||||
|
methods include GET and PROPFIND. Any implementation-defined
|
||||||
|
privilege that also controls access to GET and PROPFIND must be
|
||||||
|
aggregated under DAV:read - if an ACL grants access to DAV:read, the
|
||||||
|
client may expect that no other privilege needs to be granted to have
|
||||||
|
access to GET and PROPFIND. Additionally, the read privilege MUST
|
||||||
|
control the OPTIONS method.
|
||||||
|
*/
|
||||||
|
Read = xml.Name{"DAV:", "read"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-3.2
|
||||||
|
|
||||||
|
The write privilege controls methods that lock a resource or modify
|
||||||
|
the content, dead properties, or (in the case of a collection)
|
||||||
|
membership of the resource, such as PUT and PROPPATCH. Note that
|
||||||
|
state modification is also controlled via locking (see section 5.3 of
|
||||||
|
[RFC2518]), so effective write access requires that both write
|
||||||
|
privileges and write locking requirements are satisfied. Any
|
||||||
|
implementation-defined privilege that also controls access to methods
|
||||||
|
modifying content, dead properties or collection membership must be
|
||||||
|
aggregated under DAV:write, e.g., if an ACL grants access to
|
||||||
|
DAV:write, the client may expect that no other privilege needs to be
|
||||||
|
granted to have access to PUT and PROPPATCH.
|
||||||
|
*/
|
||||||
|
Write = xml.Name{"DAV:", "write"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-3.3
|
||||||
|
|
||||||
|
The DAV:write-properties privilege controls methods that modify the
|
||||||
|
dead properties of the resource, such as PROPPATCH. Whether this
|
||||||
|
privilege may be used to control access to any live properties is
|
||||||
|
determined by the implementation. Any implementation-defined
|
||||||
|
privilege that also controls access to methods modifying dead
|
||||||
|
properties must be aggregated under DAV:write-properties - e.g., if
|
||||||
|
an ACL grants access to DAV:write-properties, the client can safely
|
||||||
|
expect that no other privilege needs to be granted to have access to
|
||||||
|
PROPPATCH.
|
||||||
|
*/
|
||||||
|
WriteProperties = xml.Name{"DAV:", "write-properties"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-3.4
|
||||||
|
|
||||||
|
The DAV:write-content privilege controls methods that modify the
|
||||||
|
content of an existing resource, such as PUT. Any implementation-
|
||||||
|
defined privilege that also controls access to content must be
|
||||||
|
aggregated under DAV:write-content - e.g., if an ACL grants access to
|
||||||
|
DAV:write-content, the client can safely expect that no other
|
||||||
|
privilege needs to be granted to have access to PUT. Note that PUT -
|
||||||
|
when applied to an unmapped URI - creates a new resource and
|
||||||
|
therefore is controlled by the DAV:bind privilege on the parent
|
||||||
|
collection.
|
||||||
|
*/
|
||||||
|
WriteContent = xml.Name{"DAV:", "write-content"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-3.5
|
||||||
|
|
||||||
|
The DAV:unlock privilege controls the use of the UNLOCK method by a
|
||||||
|
principal other than the lock owner (the principal that created a
|
||||||
|
lock can always perform an UNLOCK). While the set of users who may
|
||||||
|
lock a resource is most commonly the same set of users who may modify
|
||||||
|
a resource, servers may allow various kinds of administrators to
|
||||||
|
unlock resources locked by others. Any privilege controlling access
|
||||||
|
by non-lock owners to UNLOCK MUST be aggregated under DAV:unlock.
|
||||||
|
|
||||||
|
A lock owner can always remove a lock by issuing an UNLOCK with the
|
||||||
|
correct lock token and authentication credentials. That is, even if
|
||||||
|
a principal does not have DAV:unlock privilege, they can still remove
|
||||||
|
locks they own. Principals other than the lock owner can remove a
|
||||||
|
lock only if they have DAV:unlock privilege and they issue an UNLOCK
|
||||||
|
with the correct lock token. Lock timeout is not affected by the
|
||||||
|
DAV:unlock privilege.
|
||||||
|
*/
|
||||||
|
Unlock = xml.Name{"DAV:", "unlock"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-3.6
|
||||||
|
|
||||||
|
The DAV:read-acl privilege controls the use of PROPFIND to retrieve
|
||||||
|
the DAV:acl property of the resource.
|
||||||
|
*/
|
||||||
|
ReadACL = xml.Name{"DAV:", "read-acl"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-3.7
|
||||||
|
|
||||||
|
The DAV:read-current-user-privilege-set privilege controls the use of
|
||||||
|
PROPFIND to retrieve the DAV:current-user-privilege-set property of
|
||||||
|
the resource.
|
||||||
|
|
||||||
|
Clients are intended to use this property to visually indicate in
|
||||||
|
their UI items that are dependent on the permissions of a resource,
|
||||||
|
for example, by graying out resources that are not writable.
|
||||||
|
|
||||||
|
This privilege is separate from DAV:read-acl because there is a need
|
||||||
|
to allow most users access to the privileges permitted the current
|
||||||
|
user (due to its use in creating the UI), while the full ACL contains
|
||||||
|
information that may not be appropriate for the current authenticated
|
||||||
|
user. As a result, the set of users who can view the full ACL is
|
||||||
|
expected to be much smaller than those who can read the current user
|
||||||
|
privilege set, and hence distinct privileges are needed for each.
|
||||||
|
*/
|
||||||
|
ReadCurrentUserPrivilegeSet = xml.Name{"DAV:", "read-current-user-privilege-set"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-3.8
|
||||||
|
|
||||||
|
The DAV:write-acl privilege controls use of the ACL method to modify
|
||||||
|
the DAV:acl property of the resource.
|
||||||
|
*/
|
||||||
|
WriteACL = xml.Name{"DAV:", "write-acl"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-3.9
|
||||||
|
|
||||||
|
The DAV:bind privilege allows a method to add a new member URL to the
|
||||||
|
specified collection (for example via PUT or MKCOL). It is ignored
|
||||||
|
for resources that are not collections.
|
||||||
|
*/
|
||||||
|
Bind = xml.Name{"DAV:", "bind"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-3.10
|
||||||
|
|
||||||
|
The DAV:unbind privilege allows a method to remove a member URL from
|
||||||
|
the specified collection (for example via DELETE or MOVE). It is
|
||||||
|
ignored for resources that are not collections.
|
||||||
|
*/
|
||||||
|
Unbind = xml.Name{"DAV:", "unbind"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-3.11
|
||||||
|
|
||||||
|
DAV:all is an aggregate privilege that contains the entire set of
|
||||||
|
privileges that can be applied to the resource.
|
||||||
|
*/
|
||||||
|
All = xml.Name{"DAV:", "all"}
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-4
|
||||||
|
|
||||||
|
rfc3744#section-5.5.1
|
||||||
|
|
||||||
|
The current user matches DAV:href only if that user is authenticated
|
||||||
|
as being (or being a member of) the principal identified by the URL
|
||||||
|
contained by that DAV:href.
|
||||||
|
|
||||||
|
The current user always matches DAV:all.
|
||||||
|
|
||||||
|
The current user matches DAV:authenticated only if authenticated.
|
||||||
|
|
||||||
|
The current user matches DAV:unauthenticated only if not
|
||||||
|
authenticated.
|
||||||
|
*/
|
||||||
|
type Principal struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: principal"`
|
||||||
|
Raw *RawXMLValue `xml:",any"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-4.1
|
||||||
|
|
||||||
|
This protected property, if non-empty, contains the URIs of network
|
||||||
|
resources with additional descriptive information about the
|
||||||
|
principal. This property identifies additional network resources
|
||||||
|
(i.e., it contains one or more URIs) that may be consulted by a
|
||||||
|
client to gain additional knowledge concerning a principal. One
|
||||||
|
expected use for this property is the storage of an LDAP [RFC2255]
|
||||||
|
scheme URL. A user-agent encountering an LDAP URL could use LDAP
|
||||||
|
[RFC2251] to retrieve additional machine-readable directory
|
||||||
|
information about the principal, and display that information in its
|
||||||
|
user interface. Support for this property is REQUIRED, and the value
|
||||||
|
is empty if no alternate URI exists for the principal.
|
||||||
|
*/
|
||||||
|
type AlternateURISet struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: alternate-URI-set"`
|
||||||
|
Href []Href `xml:"href,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-4.2
|
||||||
|
|
||||||
|
A principal may have many URLs, but there must be one "principal URL"
|
||||||
|
that clients can use to uniquely identify a principal. This
|
||||||
|
protected property contains the URL that MUST be used to identify
|
||||||
|
this principal in an ACL request. Support for this property is
|
||||||
|
REQUIRED.
|
||||||
|
*/
|
||||||
|
type PrincipalURL struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: principal-URL"`
|
||||||
|
Href Href `xml:"href,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-4.3
|
||||||
|
|
||||||
|
This property of a group principal identifies the principals that are
|
||||||
|
direct members of this group. Since a group may be a member of
|
||||||
|
another group, a group may also have indirect members (i.e., the
|
||||||
|
members of its direct members). A URL in the DAV:group-member-set
|
||||||
|
for a principal MUST be the DAV:principal-URL of that principal.
|
||||||
|
*/
|
||||||
|
type GroupMemberSet struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: group-member-set"`
|
||||||
|
Href []Href `xml:"href,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-4.4
|
||||||
|
|
||||||
|
This protected property identifies the groups in which the principal
|
||||||
|
is directly a member. Note that a server may allow a group to be a
|
||||||
|
member of another group, in which case the DAV:group-membership of
|
||||||
|
those other groups would need to be queried in order to determine the
|
||||||
|
groups in which the principal is indirectly a member. Support for
|
||||||
|
this property is REQUIRED.
|
||||||
|
*/
|
||||||
|
type GroupMembership struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: group-membership"`
|
||||||
|
Href []Href `xml:"href,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-5.1
|
||||||
|
|
||||||
|
This property identifies a particular principal as being the "owner"
|
||||||
|
of the resource. Since the owner of a resource often has special
|
||||||
|
access control capabilities (e.g., the owner frequently has permanent
|
||||||
|
DAV:write-acl privilege), clients might display the resource owner in
|
||||||
|
their user interface.
|
||||||
|
|
||||||
|
Servers MAY implement DAV:owner as protected property and MAY return
|
||||||
|
an empty DAV:owner element as property value in case no owner
|
||||||
|
information is available.
|
||||||
|
*/
|
||||||
|
type Owner struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: owner"`
|
||||||
|
Href Href `xml:"href,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-5.2
|
||||||
|
|
||||||
|
This property identifies a particular principal as being the "group"
|
||||||
|
of the resource. This property is commonly found on repositories
|
||||||
|
that implement the Unix privileges model.
|
||||||
|
|
||||||
|
Servers MAY implement DAV:group as protected property and MAY return
|
||||||
|
an empty DAV:group element as property value in case no group
|
||||||
|
information is available.
|
||||||
|
*/
|
||||||
|
type Group struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: group"`
|
||||||
|
Href Href `xml:"href,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-5.3
|
||||||
|
|
||||||
|
This is a protected property that identifies the privileges defined
|
||||||
|
for the resource.
|
||||||
|
*/
|
||||||
|
type SupportedPrivilegeSet struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: supported-privilege-set"`
|
||||||
|
SupportedPrivilege []SupportedPrivilege `xml:"supported-privilege"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-5.3
|
||||||
|
|
||||||
|
Each privilege appears as an XML element, where aggregate privileges
|
||||||
|
list as sub-elements all of the privileges that they aggregate.
|
||||||
|
*/
|
||||||
|
type SupportedPrivilege struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: supported-privilege"`
|
||||||
|
Privilege Privilege `xml:"privilege"`
|
||||||
|
/*
|
||||||
|
Abstract will be nil if not set
|
||||||
|
|
||||||
|
rfc3744#section-5.3
|
||||||
|
|
||||||
|
An abstract privilege MUST NOT be used in an ACE for that resource.
|
||||||
|
Servers MUST fail an attempt to set an abstract privilege.
|
||||||
|
*/
|
||||||
|
Abstract *struct{} `xml:"abstract,omitempty"`
|
||||||
|
Description Description `xml:"description"`
|
||||||
|
SupportedPrivilege []SupportedPrivilege `xml:"supported-privilege"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-5.3
|
||||||
|
|
||||||
|
A description is a human-readable description of what this privilege
|
||||||
|
controls access to. Servers MUST indicate the human language of the
|
||||||
|
description using the xml:lang attribute and SHOULD consider the HTTP
|
||||||
|
Accept-Language request header when selecting one of multiple
|
||||||
|
available languages.
|
||||||
|
*/
|
||||||
|
type Description struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: description"`
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
Lang string `xml:"lang,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-5.4
|
||||||
|
|
||||||
|
DAV:current-user-privilege-set is a protected property containing the
|
||||||
|
exact set of privileges (as computed by the server) granted to the
|
||||||
|
currently authenticated HTTP user. Aggregate privileges and their
|
||||||
|
contained privileges are listed. A user-agent can use the value of
|
||||||
|
this property to adjust its user interface to make actions
|
||||||
|
inaccessible (e.g., by graying out a menu item or button) for which
|
||||||
|
the current principal does not have permission. This property is
|
||||||
|
also useful for determining what operations the current principal can
|
||||||
|
perform, without having to actually execute an operation.
|
||||||
|
*/
|
||||||
|
type CurrentUserPrivilegeSet struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: current-user-privilege-set"`
|
||||||
|
Privilege []Privilege `xml:"privilege"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// convenience CurrentUserPrivilegeSet
|
||||||
|
var (
|
||||||
|
CurrentUserPrivilegeSetReadOnly = CurrentUserPrivilegeSet{
|
||||||
|
Privilege: []Privilege{NewPrivilege(Read)},
|
||||||
|
}
|
||||||
|
CurrentUserPrivilegeSetReadWrite = CurrentUserPrivilegeSet{
|
||||||
|
Privilege: []Privilege{NewPrivilege(Read), NewPrivilege(Write)},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-5.5
|
||||||
|
|
||||||
|
This is a protected property that specifies the list of access
|
||||||
|
control entries (ACEs), which define what principals are to get what
|
||||||
|
privileges for this resource.
|
||||||
|
*/
|
||||||
|
type ACL struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: acl"`
|
||||||
|
ACE []ACE `xml:"ace"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-5.5
|
||||||
|
|
||||||
|
Each DAV:ace element specifies the set of privileges to be either
|
||||||
|
granted or denied to a single principal. If the DAV:acl property is
|
||||||
|
empty, no principal is granted any privilege.
|
||||||
|
*/
|
||||||
|
type ACE struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: ace"`
|
||||||
|
/*
|
||||||
|
rfc3744#section-5.5.1
|
||||||
|
|
||||||
|
The DAV:principal element identifies the principal to which this ACE
|
||||||
|
applies.
|
||||||
|
*/
|
||||||
|
Principal Principal `xml:"principal,omitempty"`
|
||||||
|
Grant *Grant `xml:"grant,omitempty"`
|
||||||
|
Deny *Deny `xml:"deny,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-5.5.2
|
||||||
|
|
||||||
|
Each DAV:grant or DAV:deny element specifies the set of privileges to
|
||||||
|
be either granted or denied to the specified principal. A DAV:grant
|
||||||
|
or DAV:deny element of the DAV:acl of a resource MUST only contain
|
||||||
|
non-abstract elements specified in the DAV:supported-privilege-set of
|
||||||
|
that resource.
|
||||||
|
*/
|
||||||
|
type Grant struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: grant"`
|
||||||
|
Privilege []Privilege `xml:"privilege"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-5.5.2
|
||||||
|
|
||||||
|
Each DAV:grant or DAV:deny element specifies the set of privileges to
|
||||||
|
be either granted or denied to the specified principal. A DAV:grant
|
||||||
|
or DAV:deny element of the DAV:acl of a resource MUST only contain
|
||||||
|
non-abstract elements specified in the DAV:supported-privilege-set of
|
||||||
|
that resource.
|
||||||
|
*/
|
||||||
|
type Deny struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: deny"`
|
||||||
|
Privilege []Privilege `xml:"privilege"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// to be continued (5.5.2. & following)
|
||||||
|
///////////////////////////////////////////////
|
||||||
|
|
||||||
|
var (
|
||||||
|
/*
|
||||||
|
rfc3744#section-5.6.1
|
||||||
|
|
||||||
|
This element indicates that ACEs with deny clauses are not allowed.
|
||||||
|
*/
|
||||||
|
GrantOnly = xml.Name{"DAV:", "grant-only"}
|
||||||
|
/*
|
||||||
|
rfc3744#section-5.6.2
|
||||||
|
|
||||||
|
This element indicates that ACEs with the <invert> element are not
|
||||||
|
allowed.
|
||||||
|
*/
|
||||||
|
NoInvert = xml.Name{"DAV:", "no-invert"}
|
||||||
|
/*
|
||||||
|
rfc3744#section-5.6.3
|
||||||
|
|
||||||
|
This element indicates that all deny ACEs must precede all grant
|
||||||
|
ACEs.
|
||||||
|
*/
|
||||||
|
DenyBeforeGrant = xml.Name{"DAV:", "deny-before-grant"}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
/*
|
||||||
|
rfc3744#section-8.1.1
|
||||||
|
|
||||||
|
The ACEs submitted in the ACL request MUST NOT
|
||||||
|
conflict with each other. This is a catchall error code indicating
|
||||||
|
that an implementation-specific ACL restriction has been violated.
|
||||||
|
*/
|
||||||
|
NoACEConflict = xml.Name{"DAV:", "no-ace-conflict"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-8.1.1
|
||||||
|
|
||||||
|
The ACEs submitted in the ACL
|
||||||
|
request MUST NOT conflict with the protected ACEs on the resource.
|
||||||
|
For example, if the resource has a protected ACE granting DAV:write
|
||||||
|
to a given principal, then it would not be consistent if the ACL
|
||||||
|
request submitted an ACE denying DAV:write to the same principal.
|
||||||
|
*/
|
||||||
|
NoProtectedACEConflict = xml.Name{"DAV:", "no-protected-ace-conflict"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-8.1.1
|
||||||
|
|
||||||
|
The ACEs submitted in the ACL
|
||||||
|
request MUST NOT conflict with the inherited ACEs on the resource.
|
||||||
|
For example, if the resource inherits an ACE from its parent
|
||||||
|
collection granting DAV:write to a given principal, then it would not
|
||||||
|
be consistent if the ACL request submitted an ACE denying DAV:write
|
||||||
|
to the same principal. Note that reporting of this error will be
|
||||||
|
implementation-dependent. Implementations MUST either report this
|
||||||
|
error or allow the ACE to be set, and then let normal ACE evaluation
|
||||||
|
rules determine whether the new ACE has any impact on the privileges
|
||||||
|
available to a specific principal.
|
||||||
|
*/
|
||||||
|
NoInheritedACEConflict = xml.Name{"DAV:", "no-inherited-ace-conflict"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-8.1.1
|
||||||
|
|
||||||
|
The number of ACEs submitted in the ACL
|
||||||
|
request MUST NOT exceed the number of ACEs allowed on that resource.
|
||||||
|
However, ACL-compliant servers MUST support at least one ACE granting
|
||||||
|
privileges to a single principal, and one ACE granting privileges to
|
||||||
|
a group.
|
||||||
|
*/
|
||||||
|
LimitedNumberOfACEs = xml.Name{"DAV:", "limited-number-of-aces"}
|
||||||
|
|
||||||
|
// already defined above:
|
||||||
|
// DenyBeforeGrant
|
||||||
|
// GrantOnly
|
||||||
|
// NoInvert
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-8.1.1
|
||||||
|
|
||||||
|
The ACL request MUST NOT attempt to grant or deny
|
||||||
|
an abstract privilege
|
||||||
|
*/
|
||||||
|
NoAbstract = xml.Name{"DAV:", "no-abstract"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-8.1.1
|
||||||
|
|
||||||
|
The ACEs submitted in the ACL request
|
||||||
|
MUST be supported by the resource.
|
||||||
|
*/
|
||||||
|
NotSupportedPrivilege = xml.Name{"DAV:", "not-supported-privilege"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-8.1.1
|
||||||
|
|
||||||
|
The result of the ACL request MUST
|
||||||
|
have at least one ACE for each principal identified in a
|
||||||
|
DAV:required-principal XML element in the ACL semantics of that
|
||||||
|
resource
|
||||||
|
*/
|
||||||
|
MissingRequiredPrincipal = xml.Name{"DAV:", "missing-required-principal"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-8.1.1
|
||||||
|
|
||||||
|
Every principal URL in the ACL request
|
||||||
|
MUST identify a principal resource.
|
||||||
|
*/
|
||||||
|
RecognizedPrincipal = xml.Name{"DAV:", "recognized-principal"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
rfc3744#section-8.1.1
|
||||||
|
|
||||||
|
The principals specified in the ACEs
|
||||||
|
submitted in the ACL request MUST be allowed as principals for the
|
||||||
|
resource. For example, a server where only authenticated principals
|
||||||
|
can access resources would not allow the DAV:all or
|
||||||
|
DAV:unauthenticated principals to be used in an ACE, since these
|
||||||
|
would allow unauthenticated access to resources.
|
||||||
|
*/
|
||||||
|
AllowedPrincipal = xml.Name{"DAV:", "allowed-principal"}
|
||||||
|
)
|
322
internal/acl_test.go
Normal file
322
internal/acl_test.go
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func decodePropInsideMultiStatus(data []byte, v interface{}) error {
|
||||||
|
var ms MultiStatus
|
||||||
|
err := xml.Unmarshal(data, &ms)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(ms.Responses) != 1 {
|
||||||
|
return fmt.Errorf("expected 1 <response>, got %d", len(ms.Responses))
|
||||||
|
}
|
||||||
|
ps := ms.Responses[0].PropStats
|
||||||
|
if len(ps) != 1 {
|
||||||
|
return fmt.Errorf("expected 1 <propstat>, got %d", len(ps))
|
||||||
|
}
|
||||||
|
return ps[0].Prop.Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkSupportedPrivilege(t *testing.T, sp SupportedPrivilege, privilege xml.Name, abstract bool, description string, children int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if !sp.Privilege.Is(privilege) {
|
||||||
|
t.Errorf("expected %s, got %v", privilege, sp.Privilege.Raw)
|
||||||
|
}
|
||||||
|
if abstract {
|
||||||
|
if sp.Abstract == nil {
|
||||||
|
t.Errorf("missing expected <abstract>")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if sp.Abstract != nil {
|
||||||
|
t.Errorf("unexpected <abstract>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(sp.Description.Text) != description {
|
||||||
|
t.Errorf("expected description %q, got %q", description, strings.TrimSpace(sp.Description.Text))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(sp.Description.Lang) != "en" {
|
||||||
|
t.Errorf("expected lang %q, got %q", "en", sp.Description.Lang)
|
||||||
|
}
|
||||||
|
if len(sp.SupportedPrivilege) != children {
|
||||||
|
t.Fatalf("expected %d <supported-privilege>, got %d", children, len(sp.SupportedPrivilege))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestACLMarshalling(t *testing.T) {
|
||||||
|
/* rfc3744#section-5.1.1 */
|
||||||
|
t.Run("owner", func(t *testing.T) {
|
||||||
|
var owner Owner
|
||||||
|
err := decodePropInsideMultiStatus([]byte(` <?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<D:multistatus xmlns:D="DAV:">
|
||||||
|
<D:response>
|
||||||
|
<D:href>http://www.example.com/papers/</D:href>
|
||||||
|
<D:propstat>
|
||||||
|
<D:prop>
|
||||||
|
<D:owner>
|
||||||
|
<D:href>http://www.example.com/acl/users/gstein</D:href>
|
||||||
|
</D:owner>
|
||||||
|
</D:prop>
|
||||||
|
<D:status>HTTP/1.1 200 OK</D:status>
|
||||||
|
</D:propstat>
|
||||||
|
</D:response>
|
||||||
|
</D:multistatus>`), &owner)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if owner.Href.String() != "http://www.example.com/acl/users/gstein" {
|
||||||
|
t.Fatalf("expected http://www.example.com/acl/users/gstein, got %s", owner.Href.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/* rfc3744#section-5.3.1 */
|
||||||
|
t.Run("supported-privilege-set", func(t *testing.T) {
|
||||||
|
var sps SupportedPrivilegeSet
|
||||||
|
err := decodePropInsideMultiStatus([]byte(` <?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<D:multistatus xmlns:D="DAV:">
|
||||||
|
<D:response>
|
||||||
|
<D:href>http://www.example.com/papers/</D:href>
|
||||||
|
<D:propstat>
|
||||||
|
<D:prop>
|
||||||
|
<D:supported-privilege-set>
|
||||||
|
<D:supported-privilege>
|
||||||
|
<D:privilege><D:all/></D:privilege>
|
||||||
|
<D:abstract/>
|
||||||
|
<D:description xml:lang="en">
|
||||||
|
Any operation
|
||||||
|
</D:description>
|
||||||
|
<D:supported-privilege>
|
||||||
|
<D:privilege><D:read/></D:privilege>
|
||||||
|
<D:description xml:lang="en">
|
||||||
|
Read any object
|
||||||
|
</D:description>
|
||||||
|
<D:supported-privilege>
|
||||||
|
<D:privilege><D:read-acl/></D:privilege>
|
||||||
|
<D:abstract/>
|
||||||
|
<D:description xml:lang="en">Read ACL</D:description>
|
||||||
|
</D:supported-privilege>
|
||||||
|
<D:supported-privilege>
|
||||||
|
<D:privilege>
|
||||||
|
<D:read-current-user-privilege-set/>
|
||||||
|
</D:privilege>
|
||||||
|
<D:abstract/>
|
||||||
|
<D:description xml:lang="en">
|
||||||
|
Read current user privilege set property
|
||||||
|
</D:description>
|
||||||
|
</D:supported-privilege>
|
||||||
|
</D:supported-privilege>
|
||||||
|
<D:supported-privilege>
|
||||||
|
<D:privilege><D:write/></D:privilege>
|
||||||
|
<D:description xml:lang="en">
|
||||||
|
Write any object
|
||||||
|
</D:description>
|
||||||
|
<D:supported-privilege>
|
||||||
|
<D:privilege><D:write-acl/></D:privilege>
|
||||||
|
<D:description xml:lang="en">
|
||||||
|
Write ACL
|
||||||
|
</D:description>
|
||||||
|
<D:abstract/>
|
||||||
|
</D:supported-privilege>
|
||||||
|
<D:supported-privilege>
|
||||||
|
<D:privilege><D:write-properties/></D:privilege>
|
||||||
|
<D:description xml:lang="en">
|
||||||
|
Write properties
|
||||||
|
</D:description>
|
||||||
|
</D:supported-privilege>
|
||||||
|
<D:supported-privilege>
|
||||||
|
<D:privilege><D:write-content/></D:privilege>
|
||||||
|
<D:description xml:lang="en">
|
||||||
|
Write resource content
|
||||||
|
</D:description>
|
||||||
|
</D:supported-privilege>
|
||||||
|
</D:supported-privilege>
|
||||||
|
<D:supported-privilege>
|
||||||
|
<D:privilege><D:unlock/></D:privilege>
|
||||||
|
<D:description xml:lang="en">
|
||||||
|
Unlock resource
|
||||||
|
</D:description>
|
||||||
|
</D:supported-privilege>
|
||||||
|
</D:supported-privilege>
|
||||||
|
</D:supported-privilege-set>
|
||||||
|
</D:prop>
|
||||||
|
<D:status>HTTP/1.1 200 OK</D:status>
|
||||||
|
</D:propstat>
|
||||||
|
</D:response>
|
||||||
|
</D:multistatus>`), &sps)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if len(sps.SupportedPrivilege) != 1 {
|
||||||
|
t.Fatalf("expected 1 <supported-privilege>, got %d", len(sps.SupportedPrivilege))
|
||||||
|
}
|
||||||
|
sp := sps.SupportedPrivilege[0]
|
||||||
|
|
||||||
|
checkSupportedPrivilege(t, sp, All, true, "Any operation", 3)
|
||||||
|
|
||||||
|
checkSupportedPrivilege(t, sp.SupportedPrivilege[0], Read, false, "Read any object", 2)
|
||||||
|
checkSupportedPrivilege(t, sp.SupportedPrivilege[1], Write, false, "Write any object", 3)
|
||||||
|
checkSupportedPrivilege(t, sp.SupportedPrivilege[2], Unlock, false, "Unlock resource", 0)
|
||||||
|
|
||||||
|
checkSupportedPrivilege(t, sp.SupportedPrivilege[0].SupportedPrivilege[0], ReadACL, true, "Read ACL", 0)
|
||||||
|
checkSupportedPrivilege(t, sp.SupportedPrivilege[0].SupportedPrivilege[1], ReadCurrentUserPrivilegeSet, true, "Read current user privilege set property", 0)
|
||||||
|
|
||||||
|
checkSupportedPrivilege(t, sp.SupportedPrivilege[1].SupportedPrivilege[0], WriteACL, true, "Write ACL", 0)
|
||||||
|
checkSupportedPrivilege(t, sp.SupportedPrivilege[1].SupportedPrivilege[1], WriteProperties, false, "Write properties", 0)
|
||||||
|
checkSupportedPrivilege(t, sp.SupportedPrivilege[1].SupportedPrivilege[2], WriteContent, false, "Write resource content", 0)
|
||||||
|
|
||||||
|
sp = SupportedPrivilege{
|
||||||
|
Privilege: NewPrivilege(All),
|
||||||
|
Abstract: &struct{}{},
|
||||||
|
Description: Description{Text: "all"},
|
||||||
|
}
|
||||||
|
buf, err := xml.Marshal(sp)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if want := "<supported-privilege xmlns=\"DAV:\"><privilege xmlns=\"DAV:\"><all xmlns=\"DAV:\"></all></privilege><abstract></abstract><description xmlns=\"DAV:\">all</description></supported-privilege>"; string(buf) != want {
|
||||||
|
t.Errorf("expected %q, got %q", want, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
sp = SupportedPrivilege{
|
||||||
|
Privilege: NewPrivilege(Read),
|
||||||
|
Abstract: nil,
|
||||||
|
Description: Description{Text: "read"},
|
||||||
|
}
|
||||||
|
buf, err = xml.Marshal(sp)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if want := "<supported-privilege xmlns=\"DAV:\"><privilege xmlns=\"DAV:\"><read xmlns=\"DAV:\"></read></privilege><description xmlns=\"DAV:\">read</description></supported-privilege>"; string(buf) != want {
|
||||||
|
t.Errorf("expected %q, got %q", want, buf)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/* rfc3744#section-5.4.1 */
|
||||||
|
t.Run("current-user-privilege-set", func(t *testing.T) {
|
||||||
|
var cups CurrentUserPrivilegeSet
|
||||||
|
err := decodePropInsideMultiStatus([]byte(`<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<D:multistatus xmlns:D="DAV:">
|
||||||
|
<D:response>
|
||||||
|
<D:href>http://www.example.com/papers/</D:href>
|
||||||
|
<D:propstat>
|
||||||
|
<D:prop>
|
||||||
|
<D:current-user-privilege-set>
|
||||||
|
<D:privilege><D:read/></D:privilege>
|
||||||
|
</D:current-user-privilege-set>
|
||||||
|
</D:prop>
|
||||||
|
<D:status>HTTP/1.1 200 OK</D:status>
|
||||||
|
</D:propstat>
|
||||||
|
</D:response>
|
||||||
|
</D:multistatus>`), &cups)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if len(cups.Privilege) != 1 {
|
||||||
|
t.Fatalf("expected 1 <privilege>, got %d", len(cups.Privilege))
|
||||||
|
}
|
||||||
|
if !cups.Privilege[0].Is(Read) {
|
||||||
|
t.Fatalf("expected <read>, got %v", cups.Privilege[0].Raw)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
/* rfc3744#section-5.5.5 */
|
||||||
|
t.Run("acl", func(t *testing.T) {
|
||||||
|
var acl ACL
|
||||||
|
err := decodePropInsideMultiStatus([]byte(`<D:multistatus xmlns:D="DAV:">
|
||||||
|
<D:response>
|
||||||
|
<D:href>http://www.example.com/papers/</D:href>
|
||||||
|
<D:propstat>
|
||||||
|
<D:prop>
|
||||||
|
<D:acl>
|
||||||
|
<D:ace>
|
||||||
|
<D:principal>
|
||||||
|
<D:href
|
||||||
|
>http://www.example.com/acl/groups/maintainers</D:href>
|
||||||
|
</D:principal>
|
||||||
|
<D:grant>
|
||||||
|
<D:privilege><D:write/></D:privilege>
|
||||||
|
</D:grant>
|
||||||
|
</D:ace>
|
||||||
|
<D:ace>
|
||||||
|
<D:principal>
|
||||||
|
<D:all/>
|
||||||
|
</D:principal>
|
||||||
|
<D:grant>
|
||||||
|
<D:privilege><D:read/></D:privilege>
|
||||||
|
</D:grant>
|
||||||
|
</D:ace>
|
||||||
|
</D:acl>
|
||||||
|
</D:prop>
|
||||||
|
<D:status>HTTP/1.1 200 OK</D:status>
|
||||||
|
</D:propstat>
|
||||||
|
</D:response>
|
||||||
|
</D:multistatus>`), &acl)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if len(acl.ACE) != 2 {
|
||||||
|
t.Fatalf("expected 2 <ace>, got %d", len(acl.ACE))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ace := acl.ACE[0]
|
||||||
|
|
||||||
|
principalName, ok := ace.Principal.Raw.XMLName()
|
||||||
|
if want := (xml.Name{"DAV:", "href"}); !ok || principalName != want {
|
||||||
|
t.Fatalf("expected %s, got %s", want, principalName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var href Href
|
||||||
|
err = ace.Principal.Raw.Decode(&href)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if want := "http://www.example.com/acl/groups/maintainers"; href.String() != want {
|
||||||
|
t.Fatalf("expected %s, got %s", want, href.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ace.Grant.Privilege) != 1 {
|
||||||
|
t.Fatalf("expected 1 <privilege>, got %d", len(ace.Grant.Privilege))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ace.Grant.Privilege[0].Is(Write) {
|
||||||
|
t.Fatalf("expected <write>, got %v", ace.Grant.Privilege[0].Raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ace := acl.ACE[1]
|
||||||
|
|
||||||
|
principalName, ok := ace.Principal.Raw.XMLName()
|
||||||
|
if want := (xml.Name{"DAV:", "all"}); !ok || principalName != want {
|
||||||
|
t.Fatalf("expected %s, got %s", want, principalName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ace.Grant.Privilege) != 1 {
|
||||||
|
t.Fatalf("expected 1 <privilege>, got %d", len(ace.Grant.Privilege))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ace.Grant.Privilege[0].Is(Read) {
|
||||||
|
t.Fatalf("expected <read>, got %v", ace.Grant.Privilege[0].Raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ace ACE
|
||||||
|
ace.Principal.Raw = NewRawXMLElement(xml.Name{"DAV:", "authenticated"}, nil, nil)
|
||||||
|
ace.Grant = &Grant{
|
||||||
|
Privilege: []Privilege{
|
||||||
|
NewPrivilege(Read),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
buf, err := xml.Marshal(ace)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if want := "<ace xmlns=\"DAV:\"><principal xmlns=\"DAV:\"><authenticated xmlns=\"DAV:\"></authenticated></principal><grant xmlns=\"DAV:\"><privilege xmlns=\"DAV:\"><read xmlns=\"DAV:\"></read></privilege></grant></ace>"; string(buf) != want {
|
||||||
|
t.Errorf("expected %q, got %q", want, buf)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
@@ -16,7 +17,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// DiscoverContextURL performs a DNS-based CardDAV/CalDAV service discovery as
|
// DiscoverContextURL performs a DNS-based CardDAV/CalDAV service discovery as
|
||||||
// described in RFC 6352 section 11. It returns the URL to the CardDAV server.
|
// described in RFC 6764. It returns the URL to the CardDAV/CalDAV server.
|
||||||
|
// Specifically it implements points 2 and 3 from the bootstrapping procedure
|
||||||
|
// defined in RFC 6764 section 6.
|
||||||
func DiscoverContextURL(ctx context.Context, service, domain string) (string, error) {
|
func DiscoverContextURL(ctx context.Context, service, domain string) (string, error) {
|
||||||
var resolver net.Resolver
|
var resolver net.Resolver
|
||||||
|
|
||||||
@@ -31,23 +34,52 @@ func DiscoverContextURL(ctx context.Context, service, domain string) (string, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(addrs) == 0 {
|
if len(addrs) == 0 {
|
||||||
return "", fmt.Errorf("webdav: domain doesn't have an SRV record")
|
return "", errors.New("webdav: domain doesn't have an SRV record")
|
||||||
}
|
}
|
||||||
addr := addrs[0]
|
addr := addrs[0]
|
||||||
|
|
||||||
target := strings.TrimSuffix(addr.Target, ".")
|
target := strings.TrimSuffix(addr.Target, ".")
|
||||||
if target == "" {
|
if target == "" {
|
||||||
return "", fmt.Errorf("webdav: empty target in SRV record")
|
return "", errors.New("webdav: empty target in SRV record")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: perform a TXT lookup, check for a "path" key in the response
|
txtName := fmt.Sprintf("_%ss._tcp.%s", service, domain)
|
||||||
u := url.URL{Scheme: "https"}
|
txtRecords, err := resolver.LookupTXT(ctx, txtName)
|
||||||
|
if dnsErr, ok := err.(*net.DNSError); ok {
|
||||||
|
if dnsErr.IsTemporary {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var path string
|
||||||
|
switch len(txtRecords) {
|
||||||
|
case 0:
|
||||||
|
path = "/.well-known/" + service
|
||||||
|
case 1:
|
||||||
|
record := txtRecords[0]
|
||||||
|
if !strings.HasPrefix(record, "path=") {
|
||||||
|
return "", fmt.Errorf("webdav: TXT record for %s does not contain the path key", txtName)
|
||||||
|
}
|
||||||
|
|
||||||
|
path = strings.TrimPrefix(record, "path=")
|
||||||
|
if path == "" {
|
||||||
|
return "", fmt.Errorf("webdav: empty path for %s TXT record", txtName)
|
||||||
|
}
|
||||||
|
default: // more than 1
|
||||||
|
return "", fmt.Errorf("webdav: more than one entry found on %s discovery TXT record", txtName)
|
||||||
|
}
|
||||||
|
|
||||||
|
u := url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
if addr.Port == 443 {
|
if addr.Port == 443 {
|
||||||
u.Host = target
|
u.Host = target
|
||||||
} else {
|
} else {
|
||||||
u.Host = fmt.Sprintf("%v:%v", target, addr.Port)
|
u.Host = fmt.Sprintf("%v:%v", target, addr.Port)
|
||||||
}
|
}
|
||||||
u.Path = "/.well-known/" + service
|
|
||||||
return u.String(), nil
|
return u.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -21,7 +21,8 @@ var (
|
|||||||
GetLastModifiedName = xml.Name{Namespace, "getlastmodified"}
|
GetLastModifiedName = xml.Name{Namespace, "getlastmodified"}
|
||||||
GetETagName = xml.Name{Namespace, "getetag"}
|
GetETagName = xml.Name{Namespace, "getetag"}
|
||||||
|
|
||||||
CurrentUserPrincipalName = xml.Name{Namespace, "current-user-principal"}
|
CurrentUserPrincipalName = xml.Name{Namespace, "current-user-principal"}
|
||||||
|
CurrentUserPrivilegeSetName = xml.Name{Namespace, "current-user-privilege-set"}
|
||||||
)
|
)
|
||||||
|
|
||||||
type Status struct {
|
type Status struct {
|
||||||
|
@@ -64,7 +64,7 @@ func ServeMultiStatus(w http.ResponseWriter, ms *MultiStatus) error {
|
|||||||
type Backend interface {
|
type Backend interface {
|
||||||
Options(r *http.Request) (caps []string, allow []string, err error)
|
Options(r *http.Request) (caps []string, allow []string, err error)
|
||||||
HeadGet(w http.ResponseWriter, r *http.Request) error
|
HeadGet(w http.ResponseWriter, r *http.Request) error
|
||||||
PropFind(r *http.Request, pf *PropFind, depth Depth) (*MultiStatus, error)
|
PropFind(w http.ResponseWriter, r *http.Request, pf *PropFind, depth Depth) error
|
||||||
PropPatch(r *http.Request, pu *PropertyUpdate) (*Response, error)
|
PropPatch(r *http.Request, pu *PropertyUpdate) (*Response, error)
|
||||||
Put(w http.ResponseWriter, r *http.Request) error
|
Put(w http.ResponseWriter, r *http.Request) error
|
||||||
Delete(r *http.Request) error
|
Delete(r *http.Request) error
|
||||||
@@ -152,12 +152,7 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ms, err := h.Backend.PropFind(r, &propfind, depth)
|
return h.Backend.PropFind(w, r, &propfind, depth)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ServeMultiStatus(w, ms)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PropFindFunc func(raw *RawXMLValue) (interface{}, error)
|
type PropFindFunc func(raw *RawXMLValue) (interface{}, error)
|
||||||
|
59
server.go
59
server.go
@@ -115,39 +115,39 @@ func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth internal.Depth) (*internal.MultiStatus, error) {
|
func (b *backend) PropFind(w http.ResponseWriter, r *http.Request, propfind *internal.PropFind, depth internal.Depth) error {
|
||||||
// TODO: use partial error Response on error
|
// TODO: use partial error Response on error
|
||||||
|
|
||||||
fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path)
|
fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var resps []internal.Response
|
var resps []internal.Response
|
||||||
if depth != internal.DepthZero && fi.IsDir {
|
if depth != internal.DepthZero && fi.IsDir {
|
||||||
children, err := b.FileSystem.ReadDir(r.Context(), r.URL.Path, depth == internal.DepthInfinity)
|
children, err := b.FileSystem.ReadDir(r.Context(), r.URL.Path, depth == internal.DepthInfinity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resps = make([]internal.Response, len(children))
|
resps = make([]internal.Response, len(children))
|
||||||
for i, child := range children {
|
for i, child := range children {
|
||||||
resp, err := b.propFindFile(propfind, &child)
|
resp, err := b.propFindFile(propfind, &child)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
resps[i] = *resp
|
resps[i] = *resp
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resp, err := b.propFindFile(propfind, fi)
|
resp, err := b.propFindFile(propfind, fi)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resps = []internal.Response{*resp}
|
resps = []internal.Response{*resp}
|
||||||
}
|
}
|
||||||
|
|
||||||
return internal.NewMultiStatus(resps...), nil
|
return internal.ServeMultiStatus(w, internal.NewMultiStatus(resps...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) propFindFile(propfind *internal.PropFind, fi *FileInfo) (*internal.Response, error) {
|
func (b *backend) propFindFile(propfind *internal.PropFind, fi *FileInfo) (*internal.Response, error) {
|
||||||
@@ -189,8 +189,51 @@ func (b *backend) propFindFile(propfind *internal.PropFind, fi *FileInfo) (*inte
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) PropPatch(r *http.Request, update *internal.PropertyUpdate) (*internal.Response, error) {
|
func (b *backend) PropPatch(r *http.Request, update *internal.PropertyUpdate) (*internal.Response, error) {
|
||||||
// TODO: return a failed Response instead
|
fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path)
|
||||||
return nil, internal.HTTPErrorf(http.StatusForbidden, "webdav: PROPPATCH is unsupported")
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &internal.Response{Hrefs: []internal.Href{internal.Href{Path: fi.Path}}}
|
||||||
|
|
||||||
|
for _, set := range update.Set {
|
||||||
|
for _, raw := range set.Prop.Raw {
|
||||||
|
xmlName, ok := raw.XMLName()
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyVal := internal.NewRawXMLElement(xmlName, nil, nil)
|
||||||
|
|
||||||
|
if err := resp.EncodeProp(http.StatusForbidden, emptyVal); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, remove := range update.Remove {
|
||||||
|
for _, raw := range remove.Prop.Raw {
|
||||||
|
xmlName, ok := raw.XMLName()
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyVal := internal.NewRawXMLElement(xmlName, nil, nil)
|
||||||
|
|
||||||
|
if err := resp.EncodeProp(http.StatusForbidden, emptyVal); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.PropStats) == 0 {
|
||||||
|
return nil, internal.HTTPErrorf(http.StatusBadRequest,
|
||||||
|
"webdav: request missing properties to update")
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) Put(w http.ResponseWriter, r *http.Request) error {
|
func (b *backend) Put(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
Reference in New Issue
Block a user