From 3a24ef44799cf0a3695f2c68ff7cfa1ad3a65006 Mon Sep 17 00:00:00 2001 From: oliverpool Date: Sun, 6 Oct 2024 17:48:39 +0200 Subject: [PATCH 1/5] internal: start acl --- internal/acl.go | 538 +++++++++++++++++++++++++++++++++++++++++++ internal/acl_test.go | 322 ++++++++++++++++++++++++++ 2 files changed, 860 insertions(+) create mode 100644 internal/acl.go create mode 100644 internal/acl_test.go diff --git a/internal/acl.go b/internal/acl.go new file mode 100644 index 0000000..ad6f0b5 --- /dev/null +++ b/internal/acl.go @@ -0,0 +1,538 @@ +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"` + SupportedPrivileges []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"` + SupportedPrivileges []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"` + Privileges []Privilege `xml:"privilege"` +} + +/* +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"` + Privileges []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"` + Privileges []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..5fa77a3 --- /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.SupportedPrivileges) != children { + t.Fatalf("expected %d , got %d", children, len(sp.SupportedPrivileges)) + } +} + +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.SupportedPrivileges) != 1 { + t.Fatalf("expected 1 , got %d", len(sps.SupportedPrivileges)) + } + sp := sps.SupportedPrivileges[0] + + checkSupportedPrivilege(t, sp, All, true, "Any operation", 3) + + checkSupportedPrivilege(t, sp.SupportedPrivileges[0], Read, false, "Read any object", 2) + checkSupportedPrivilege(t, sp.SupportedPrivileges[1], Write, false, "Write any object", 3) + checkSupportedPrivilege(t, sp.SupportedPrivileges[2], Unlock, false, "Unlock resource", 0) + + checkSupportedPrivilege(t, sp.SupportedPrivileges[0].SupportedPrivileges[0], ReadAcl, true, "Read ACL", 0) + checkSupportedPrivilege(t, sp.SupportedPrivileges[0].SupportedPrivileges[1], ReadCurrentUserPrivilegeSet, true, "Read current user privilege set property", 0) + + checkSupportedPrivilege(t, sp.SupportedPrivileges[1].SupportedPrivileges[0], WriteAcl, true, "Write ACL", 0) + checkSupportedPrivilege(t, sp.SupportedPrivileges[1].SupportedPrivileges[1], WriteProperties, false, "Write properties", 0) + checkSupportedPrivilege(t, sp.SupportedPrivileges[1].SupportedPrivileges[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.Privileges) != 1 { + t.Fatalf("expected 1 , got %d", len(cups.Privileges)) + } + if !cups.Privileges[0].Is(Read) { + t.Fatalf("expected , got %v", cups.Privileges[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.Privileges) != 1 { + t.Fatalf("expected 1 , got %d", len(ace.Grant.Privileges)) + } + + if !ace.Grant.Privileges[0].Is(Write) { + t.Fatalf("expected , got %v", ace.Grant.Privileges[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.Privileges) != 1 { + t.Fatalf("expected 1 , got %d", len(ace.Grant.Privileges)) + } + + if !ace.Grant.Privileges[0].Is(Read) { + t.Fatalf("expected , got %v", ace.Grant.Privileges[0].Raw) + } + } + + var ace ACE + ace.Principal.Raw = NewRawXMLElement(xml.Name{"DAV:", "authenticated"}, nil, nil) + ace.Grant = &Grant{ + Privileges: []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) + } + }) +} From 3f534b9d0178d3bffda1caf1f3ab753cc0f0496f Mon Sep 17 00:00:00 2001 From: oliverpool Date: Sun, 6 Oct 2024 22:37:36 +0200 Subject: [PATCH 2/5] fix casing --- internal/acl.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/acl.go b/internal/acl.go index ad6f0b5..f53b648 100644 --- a/internal/acl.go +++ b/internal/acl.go @@ -109,7 +109,7 @@ var ( 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"} + ReadACL = xml.Name{"DAV:", "read-acl"} /* rfc3744#section-3.7 @@ -138,7 +138,7 @@ var ( 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"} + WriteACL = xml.Name{"DAV:", "write-acl"} /* rfc3744#section-3.9 @@ -445,7 +445,7 @@ var ( 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"} + NoACEConflict = xml.Name{"DAV:", "no-ace-conflict"} /* rfc3744#section-8.1.1 @@ -456,7 +456,7 @@ var ( 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"} + NoProtectedACEConflict = xml.Name{"DAV:", "no-protected-ace-conflict"} /* rfc3744#section-8.1.1 @@ -472,7 +472,7 @@ var ( 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"} + NoInheritedACEConflict = xml.Name{"DAV:", "no-inherited-ace-conflict"} /* rfc3744#section-8.1.1 @@ -483,7 +483,7 @@ var ( privileges to a single principal, and one ACE granting privileges to a group. */ - LimitedNumberOfAces = xml.Name{"DAV:", "limited-number-of-aces"} + LimitedNumberOfACEs = xml.Name{"DAV:", "limited-number-of-aces"} // already defined above: // DenyBeforeGrant From 98efdd7e320a382ac4306e47130f1ee78baa9e41 Mon Sep 17 00:00:00 2001 From: oliverpool Date: Sun, 6 Oct 2024 22:40:19 +0200 Subject: [PATCH 3/5] fix casing test --- internal/acl_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/acl_test.go b/internal/acl_test.go index 5fa77a3..98f4352 100644 --- a/internal/acl_test.go +++ b/internal/acl_test.go @@ -163,10 +163,10 @@ func TestACLMarshalling(t *testing.T) { checkSupportedPrivilege(t, sp.SupportedPrivileges[1], Write, false, "Write any object", 3) checkSupportedPrivilege(t, sp.SupportedPrivileges[2], Unlock, false, "Unlock resource", 0) - checkSupportedPrivilege(t, sp.SupportedPrivileges[0].SupportedPrivileges[0], ReadAcl, true, "Read ACL", 0) + checkSupportedPrivilege(t, sp.SupportedPrivileges[0].SupportedPrivileges[0], ReadACL, true, "Read ACL", 0) checkSupportedPrivilege(t, sp.SupportedPrivileges[0].SupportedPrivileges[1], ReadCurrentUserPrivilegeSet, true, "Read current user privilege set property", 0) - checkSupportedPrivilege(t, sp.SupportedPrivileges[1].SupportedPrivileges[0], WriteAcl, true, "Write ACL", 0) + checkSupportedPrivilege(t, sp.SupportedPrivileges[1].SupportedPrivileges[0], WriteACL, true, "Write ACL", 0) checkSupportedPrivilege(t, sp.SupportedPrivileges[1].SupportedPrivileges[1], WriteProperties, false, "Write properties", 0) checkSupportedPrivilege(t, sp.SupportedPrivileges[1].SupportedPrivileges[2], WriteContent, false, "Write resource content", 0) From ac76437630fd52cd7ef19c678c144e2d81d58bb4 Mon Sep 17 00:00:00 2001 From: oliverpool Date: Tue, 8 Oct 2024 18:42:40 +0200 Subject: [PATCH 4/5] singular --- internal/acl.go | 22 +++++++++---------- internal/acl_test.go | 52 ++++++++++++++++++++++---------------------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/internal/acl.go b/internal/acl.go index f53b648..24d29e1 100644 --- a/internal/acl.go +++ b/internal/acl.go @@ -292,8 +292,8 @@ This is a protected property that identifies the privileges defined for the resource. */ type SupportedPrivilegeSet struct { - XMLName xml.Name `xml:"DAV: supported-privilege-set"` - SupportedPrivileges []SupportedPrivilege `xml:"supported-privilege"` + XMLName xml.Name `xml:"DAV: supported-privilege-set"` + SupportedPrivilege []SupportedPrivilege `xml:"supported-privilege"` } /* @@ -313,9 +313,9 @@ type SupportedPrivilege struct { 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"` - SupportedPrivileges []SupportedPrivilege `xml:"supported-privilege"` + Abstract *struct{} `xml:"abstract,omitempty"` + Description Description `xml:"description"` + SupportedPrivilege []SupportedPrivilege `xml:"supported-privilege"` } /* @@ -347,8 +347,8 @@ 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"` - Privileges []Privilege `xml:"privilege"` + XMLName xml.Name `xml:"DAV: current-user-privilege-set"` + Privilege []Privilege `xml:"privilege"` } /* @@ -393,8 +393,8 @@ non-abstract elements specified in the DAV:supported-privilege-set of that resource. */ type Grant struct { - XMLName xml.Name `xml:"DAV: grant"` - Privileges []Privilege `xml:"privilege"` + XMLName xml.Name `xml:"DAV: grant"` + Privilege []Privilege `xml:"privilege"` } /* @@ -407,8 +407,8 @@ non-abstract elements specified in the DAV:supported-privilege-set of that resource. */ type Deny struct { - XMLName xml.Name `xml:"DAV: deny"` - Privileges []Privilege `xml:"privilege"` + XMLName xml.Name `xml:"DAV: deny"` + Privilege []Privilege `xml:"privilege"` } // to be continued (5.5.2. & following) diff --git a/internal/acl_test.go b/internal/acl_test.go index 98f4352..0a8a1b2 100644 --- a/internal/acl_test.go +++ b/internal/acl_test.go @@ -44,8 +44,8 @@ func checkSupportedPrivilege(t *testing.T, sp SupportedPrivilege, privilege xml. if strings.TrimSpace(sp.Description.Lang) != "en" { t.Errorf("expected lang %q, got %q", "en", sp.Description.Lang) } - if len(sp.SupportedPrivileges) != children { - t.Fatalf("expected %d , got %d", children, len(sp.SupportedPrivileges)) + if len(sp.SupportedPrivilege) != children { + t.Fatalf("expected %d , got %d", children, len(sp.SupportedPrivilege)) } } @@ -152,23 +152,23 @@ func TestACLMarshalling(t *testing.T) { if err != nil { t.Error(err) } - if len(sps.SupportedPrivileges) != 1 { - t.Fatalf("expected 1 , got %d", len(sps.SupportedPrivileges)) + if len(sps.SupportedPrivilege) != 1 { + t.Fatalf("expected 1 , got %d", len(sps.SupportedPrivilege)) } - sp := sps.SupportedPrivileges[0] + sp := sps.SupportedPrivilege[0] checkSupportedPrivilege(t, sp, All, true, "Any operation", 3) - checkSupportedPrivilege(t, sp.SupportedPrivileges[0], Read, false, "Read any object", 2) - checkSupportedPrivilege(t, sp.SupportedPrivileges[1], Write, false, "Write any object", 3) - checkSupportedPrivilege(t, sp.SupportedPrivileges[2], Unlock, false, "Unlock resource", 0) + 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.SupportedPrivileges[0].SupportedPrivileges[0], ReadACL, true, "Read ACL", 0) - checkSupportedPrivilege(t, sp.SupportedPrivileges[0].SupportedPrivileges[1], ReadCurrentUserPrivilegeSet, true, "Read current user privilege set property", 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.SupportedPrivileges[1].SupportedPrivileges[0], WriteACL, true, "Write ACL", 0) - checkSupportedPrivilege(t, sp.SupportedPrivileges[1].SupportedPrivileges[1], WriteProperties, false, "Write properties", 0) - checkSupportedPrivilege(t, sp.SupportedPrivileges[1].SupportedPrivileges[2], WriteContent, false, "Write resource content", 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), @@ -217,11 +217,11 @@ func TestACLMarshalling(t *testing.T) { if err != nil { t.Error(err) } - if len(cups.Privileges) != 1 { - t.Fatalf("expected 1 , got %d", len(cups.Privileges)) + if len(cups.Privilege) != 1 { + t.Fatalf("expected 1 , got %d", len(cups.Privilege)) } - if !cups.Privileges[0].Is(Read) { - t.Fatalf("expected , got %v", cups.Privileges[0].Raw) + if !cups.Privilege[0].Is(Read) { + t.Fatalf("expected , got %v", cups.Privilege[0].Raw) } }) /* rfc3744#section-5.5.5 */ @@ -279,12 +279,12 @@ func TestACLMarshalling(t *testing.T) { t.Fatalf("expected %s, got %s", want, href.String()) } - if len(ace.Grant.Privileges) != 1 { - t.Fatalf("expected 1 , got %d", len(ace.Grant.Privileges)) + if len(ace.Grant.Privilege) != 1 { + t.Fatalf("expected 1 , got %d", len(ace.Grant.Privilege)) } - if !ace.Grant.Privileges[0].Is(Write) { - t.Fatalf("expected , got %v", ace.Grant.Privileges[0].Raw) + if !ace.Grant.Privilege[0].Is(Write) { + t.Fatalf("expected , got %v", ace.Grant.Privilege[0].Raw) } } { @@ -295,19 +295,19 @@ func TestACLMarshalling(t *testing.T) { t.Fatalf("expected %s, got %s", want, principalName) } - if len(ace.Grant.Privileges) != 1 { - t.Fatalf("expected 1 , got %d", len(ace.Grant.Privileges)) + if len(ace.Grant.Privilege) != 1 { + t.Fatalf("expected 1 , got %d", len(ace.Grant.Privilege)) } - if !ace.Grant.Privileges[0].Is(Read) { - t.Fatalf("expected , got %v", ace.Grant.Privileges[0].Raw) + 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{ - Privileges: []Privilege{ + Privilege: []Privilege{ NewPrivilege(Read), }, } From 539cb7ed90ce6213154cdb17961c3aefb36919d7 Mon Sep 17 00:00:00 2001 From: oliverpool Date: Tue, 8 Oct 2024 18:50:03 +0200 Subject: [PATCH 5/5] carddav: ReadOnly property --- carddav/carddav.go | 2 ++ carddav/server.go | 12 ++++++++++++ internal/acl.go | 10 ++++++++++ internal/elements.go | 3 ++- 4 files changed, 26 insertions(+), 1 deletion(-) 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 index 24d29e1..a2cf7b4 100644 --- a/internal/acl.go +++ b/internal/acl.go @@ -351,6 +351,16 @@ type CurrentUserPrivilegeSet struct { 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 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 {