diff --git a/carddav/carddav.go b/carddav/carddav.go index ebfd66d..f314aed 100644 --- a/carddav/carddav.go +++ b/carddav/carddav.go @@ -28,6 +28,7 @@ type AddressBook struct { Description string MaxResourceSize int64 SupportedAddressData []AddressDataType + ReadOnly bool } func (ab *AddressBook) SupportsAddressData(contentType, version string) bool { @@ -107,6 +108,7 @@ type AddressObject struct { ContentLength int64 ETag string Card vcard.Card + ReadOnly bool } // SyncQuery is the query struct represents a sync-collection request diff --git a/carddav/server.go b/carddav/server.go index 915c49e..d91efc9 100644 --- a/carddav/server.go +++ b/carddav/server.go @@ -507,6 +507,12 @@ func (b *backend) propFindAddressBook(ctx context.Context, propfind *internal.Pr }, }, nil }, + internal.CurrentUserPrivilegeSetName: func(*internal.RawXMLValue) (interface{}, error) { + if ab.ReadOnly { + return &internal.CurrentUserPrivilegeSetReadOnly, nil + } + return &internal.CurrentUserPrivilegeSetReadWrite, nil + }, } if ab.Name != "" { @@ -573,6 +579,12 @@ func (b *backend) propFindAddressObject(ctx context.Context, propfind *internal. 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 { diff --git a/internal/acl.go b/internal/acl.go new file mode 100644 index 0000000..a2cf7b4 --- /dev/null +++ b/internal/acl.go @@ -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 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"} +) diff --git a/internal/acl_test.go b/internal/acl_test.go new file mode 100644 index 0000000..0a8a1b2 --- /dev/null +++ b/internal/acl_test.go @@ -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 , got %d", len(ms.Responses)) + } + ps := ms.Responses[0].PropStats + if len(ps) != 1 { + return fmt.Errorf("expected 1 , 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 ") + } + } else { + if sp.Abstract != nil { + t.Errorf("unexpected ") + } + } + 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 , 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(` + + + http://www.example.com/papers/ + + + + http://www.example.com/acl/users/gstein + + + HTTP/1.1 200 OK + + + `), &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(` + + + http://www.example.com/papers/ + + + + + + + + Any operation + + + + + Read any object + + + + + Read ACL + + + + + + + + Read current user privilege set property + + + + + + + Write any object + + + + + Write ACL + + + + + + + Write properties + + + + + + Write resource content + + + + + + + Unlock resource + + + + + + HTTP/1.1 200 OK + + + `), &sps) + if err != nil { + t.Error(err) + } + if len(sps.SupportedPrivilege) != 1 { + t.Fatalf("expected 1 , 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 := "all"; 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 := "read"; 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(` + + + http://www.example.com/papers/ + + + + + + + HTTP/1.1 200 OK + + +`), &cups) + if err != nil { + t.Error(err) + } + if len(cups.Privilege) != 1 { + t.Fatalf("expected 1 , got %d", len(cups.Privilege)) + } + if !cups.Privilege[0].Is(Read) { + t.Fatalf("expected , got %v", cups.Privilege[0].Raw) + } + }) + /* rfc3744#section-5.5.5 */ + t.Run("acl", func(t *testing.T) { + var acl ACL + err := decodePropInsideMultiStatus([]byte(` + + http://www.example.com/papers/ + + + + + + http://www.example.com/acl/groups/maintainers + + + + + + + + + + + + + + + + HTTP/1.1 200 OK + + + `), &acl) + if err != nil { + t.Error(err) + } + if len(acl.ACE) != 2 { + t.Fatalf("expected 2 , 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 , got %d", len(ace.Grant.Privilege)) + } + + if !ace.Grant.Privilege[0].Is(Write) { + t.Fatalf("expected , 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 , got %d", len(ace.Grant.Privilege)) + } + + if !ace.Grant.Privilege[0].Is(Read) { + t.Fatalf("expected , 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 := ""; string(buf) != want { + t.Errorf("expected %q, got %q", want, buf) + } + }) +} diff --git a/internal/elements.go b/internal/elements.go index db7d960..ca71aa8 100644 --- a/internal/elements.go +++ b/internal/elements.go @@ -21,7 +21,8 @@ var ( GetLastModifiedName = xml.Name{Namespace, "getlastmodified"} 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 {