22
33declare (strict_types=1 );
44/**
5- * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
5+ * SPDX-FileCopyrightText: 2019-2025 Nextcloud GmbH and Nextcloud contributors
66 * SPDX-License-Identifier: AGPL-3.0-or-later
77 */
88
3131use Sabre \Xml \Reader ;
3232
3333/**
34- * SabreDAV plugin for exposing and updating advanced ACL properties.
34+ * SabreDAV plugin for exposing ACL ( advanced permissions) properties.
3535 *
3636 * Handles WebDAV PROPFIND and PROPPATCH events for Nextcloud Teams/Group Folders with granular access controls.
3737 *
@@ -57,7 +57,7 @@ class ACLPlugin extends ServerPlugin {
5757
5858 private ?Server $ server = null ;
5959 private ?IUser $ user = null ;
60- /** @var array<int, bool> */
60+ /** @var array<int, bool> Folder ID => can manage ACLs */
6161 private array $ canManageACLForFolder = [];
6262
6363 public function __construct (
@@ -73,7 +73,7 @@ public function __construct(
7373 public function initialize (Server $ server ): void {
7474 $ this ->server = $ server ;
7575
76- // may be null for public links / federated shares; handler logic must account for this.
76+ // Note: a null user is permitted (i.e. for public links / federated shares) ; handler logic must account for this.
7777 $ this ->user = $ this ->userSession ->getUser ();
7878
7979 $ this ->server ->on ('propFind ' , $ this ->propFind (...));
@@ -87,14 +87,9 @@ public function initialize(Server $server): void {
8787 }
8888
8989 /**
90- * Property request handlers.
91- *
92- * These handlers provide read-only access to ACL related information.
93- *
94- * In the current implementation, individual property level handlers that depend on $this->user
95- * are expected to determine and return an appropriate safe value when $this->user is null
96- * (e.g., for public links or federated shares).
97- *
90+ * WebDAV PROPFIND event handler for ACL-related properties.
91+ * Provides read-only access to ACL information for the current node.
92+ * If the session is unauthenticated, safe defaults are returned.
9893 */
9994 public function propFind (PropFind $ propFind , INode $ node ): void {
10095 if (!$ node instanceof Node) {
@@ -107,55 +102,25 @@ public function propFind(PropFind $propFind, INode $node): void {
107102 return ;
108103 }
109104
110- /*
111- * Handler to return the direct ACL rules for a specific file or folder via a WebDAV property request.
112- *
113- * - Direct ACL rules are those assigned directly to a specific file or folder (i.e. regardless of inheritance)
114- * - Admins or managers set these rules on individual nodes (files or folders).
115- * - Rules grant/restrict permissions for specific entities (users/groups/teams) for only that exact node.
116- *
117- * Example: If you set a rule to allow "Group Xβ to write to the folder `/Documents/Reports`,
118- * that is a direct ACL rule for `/Documents/Reports`.
119- *
120- * Note: Even if permission is granted directly to a child, if a parent folder does not grant read/list, the
121- * child will remain inaccessible and invisible to the user.
122- *
123- * Returns an empty array if non-user sessions.
124- */
105+ // Handler to provide ACL rules directly assigned to the file or folder.
125106 $ propFind ->handle (
126107 self ::ACL_LIST ,
127108 fn () => $ this ->getDirectAclRulesForPath ($ fileInfo , $ mount )
128109 );
129110
130- /*
131- * Handler to return the inherited (effective) ACL rules for a file or folder via a WebDAV property request.
132- *
133- * Inherited (effective) ACL rules:
134- * - are those that apply to a file or folder because they were set on one of its parent folders.
135- * - are not set directly on the node in question -- they "cascade down" from parent directories with
136- * specific ACLs.
137- * - influence the effective permissions on a node by combining the rules set on its parent directories.
138- *
139- * Example: If `/Documents` grants "Group Y" read access, then `/Documents/Reports/file.txt` inherits that
140- * permission even if no direct rule exists for `/Documents/Reports/file.txt`.
141- *
142- * Note: Even if permission is granted directly to a child, if a parent folder does not grant read/list, the
143- * child is inaccessible and invisible to the user.
144- *
145- * Returns an empty array if non-user sessions.
146- */
111+ // Handler to provide the ACL rules inherited from parent folders (not set directly).
147112 $ propFind ->handle (
148113 self ::INHERITED_ACL_LIST ,
149114 fn () => $ this ->getInheritedAclRulesForPath ($ fileInfo , $ mount )
150115 );
151116
152- // Handler to provide the group folder ID for the current file or folder as a WebDAV property
117+ // Handler to provide the group folder ID for the current file or folder.
153118 $ propFind ->handle (
154119 self ::GROUP_FOLDER_ID ,
155120 fn (): int => $ this ->folderManager ->getFolderByPath ($ fileInfo ->getPath ())
156121 );
157122
158- // Handler to provide whether ACLs are enabled for the current group folder as a WebDAV property
123+ // Handler to provide whether ACLs are enabled for the current group folder.
159124 $ propFind ->handle (
160125 self ::ACL_ENABLED ,
161126 function () use ($ fileInfo ): bool {
@@ -164,23 +129,23 @@ function () use ($fileInfo): bool {
164129 }
165130 );
166131
167- // Handler to determine if the current user can manage ACLs for this group folder and return as a WebDAV property
132+ // Handler to determine and return if the current user can manage ACLs for this group folder.
168133 $ propFind ->handle (
169134 self ::ACL_CAN_MANAGE ,
170135 function () use ($ fileInfo ): bool {
171- // Fail softly for non-user sessions
136+ // Gracefully handle non-user sessions
172137 if ($ this ->user === null ) {
173138 return false ;
174139 }
175140 return $ this ->isAdmin ($ this ->user , $ fileInfo ->getPath ());
176141 }
177142 );
178143
179- // Handler to provide the effective base permissions for the current group folder as a WebDAV property
144+ // Handler to provide the effective base permissions for the current group folder.
180145 $ propFind ->handle (
181146 self ::ACL_BASE_PERMISSION_PROPERTYNAME ,
182147 function () use ($ mount ): int {
183- // Fail softly for non-user sessions
148+ // Gracefully handle non-user sessions
184149 if ($ this ->user === null ) {
185150 return Constants::PERMISSION_ALL ;
186151 }
@@ -192,9 +157,8 @@ function () use ($mount): int {
192157 }
193158
194159 /**
195- * Property update handlers.
196- *
197- * These handlers enable modifying ACL related configuration.
160+ * WebDAV PROPPATCH event handler for ACL-related properties.
161+ * Enables modification of ACL assignments if the user has admin rights on the current node.
198162 */
199163 public function propPatch (string $ path , PropPatch $ propPatch ): void {
200164 if ($ this ->server === null ) {
@@ -219,20 +183,36 @@ public function propPatch(string $path, PropPatch $propPatch): void {
219183 return ;
220184 }
221185
222- // Only allow ACL modifications if the current user has admin rights for this group folder
186+ // Only allow if user has admin rights for this group folder
223187 if (!$ this ->isAdmin ($ this ->user , $ fileInfo ->getPath ())) {
224188 return ;
225189 }
226190
227- // Handler to process and save changes to a folder's ACL rules via a WebDAV property update
191+ // Handler to update (replace) the direct ACL rules for the specified file/folder.
228192 $ propPatch ->handle (
229193 self ::ACL_LIST ,
230194 fn (array $ submittedRules ) => $ this ->updateAclRulesForPath ($ submittedRules , $ node , $ fileInfo , $ mount )
231195 );
232196 }
233197
198+ /**
199+ * Retrieves ACL rules assigned directly (not inherited) to the given file or folder.
200+ *
201+ * - For admins/managers: returns all direct rules for the node.
202+ * - For non-admins/users: returns only direct rules relevant to the user.
203+ * - For public/federated sessions: returns an empty array.
204+ *
205+ * Example: Granting "Group X" write access to `/Documents/Reports` is a direct ACL rule for that folder.
206+ *
207+ * Note: Direct rules alone do not determine effective access:
208+ * - Read/list access must be permitted by all parent folders, regardless of direct rules.
209+ * - For other permissions, direct rules take priority; missing permissions may be filled via inheritance.
210+ *
211+ * Example: If "Group X" has no read access on `/Documents`, they still can't access `/Documents/Reports`.
212+ *
213+ * @return list<Rule>|null Direct ACL rules for the file/folder (may be empty).
214+ */
234215 private function getDirectAclRulesForPath (FileInfo $ fileInfo , GroupMountPoint $ mount ): ?array {
235- // Fail softly for non-user sessions
236216 if ($ this ->user === null ) {
237217 return [];
238218 }
@@ -259,6 +239,19 @@ private function getDirectAclRulesForPath(FileInfo $fileInfo, GroupMountPoint $m
259239 return array_pop ($ rules );
260240 }
261241
242+ /**
243+ * Retrieves ACL rules inherited from parent folders (not set directly) for the given file or folder.
244+ *
245+ * - For admins/managers: returns all inherited rules affecting the node.
246+ * - For non-admins/users: returns only inherited rules relevant to the user.
247+ * - For public/federated sessions: returns an empty array.
248+ *
249+ * Note: Inherited rules are merged from all ancestor folders; direct rules for this node are excluded.
250+ * - Effective read/list access requires all parent folders to permit access.
251+ * - Other permissions are determined by merging inherited and direct rules separately.
252+ *
253+ * @return list<Rule> Inherited ACL rules affecting the file/folder (may be empty).
254+ */
262255 private function getInheritedAclRulesForPath (FileInfo $ fileInfo , GroupMountPoint $ mount ): array {
263256 // Fail softly for non-user sessions
264257 if ($ this ->user === null ) {
@@ -354,7 +347,22 @@ private function getInheritedAclRulesForPath(FileInfo $fileInfo, GroupMountPoint
354347 }
355348
356349 /**
357- * @throws BadRequest
350+ * Update (replace) the entire set of direct ACL rules for the given file or folder.
351+ *
352+ * This method overwrites all existing direct ACL rules for the node with a new set.
353+ * Only users with ACL management rights (admins/managers) can perform this operation.
354+ *
355+ * - All provided Rule objects are written as the new direct ACLs for this file/folder.
356+ * - Inherited ACL rules from parent folders are not modified.
357+ * - If the rules array is empty, all direct ACLs on this node are removed.
358+ * - Logs critical actions and dispatches audit events for ACL changes.
359+ *
360+ * @param Rule[] $submittedRules Array of new direct ACL Rule objects to apply.
361+ * @param $node object of file/folder being updated.
362+ * @param FileInfo $fileInfo object of file or folder being updated.
363+ * @param GroupMountPoint $mount context for storage and folder resolution.
364+ *
365+ * @throws BadRequest if the operation is invalid (e.g. user's own read access would be removed)
358366 */
359367 private function updateAclRulesForPath (array $ submittedRules , Node $ node , FileInfo $ fileInfo , GroupMountPoint $ mount ): bool {
360368 $ aclRelativePath = trim ($ mount ->getSourcePath () . '/ ' . $ fileInfo ->getInternalPath (), '/ ' );
@@ -463,13 +471,12 @@ private function updateAclRulesForPath(array $submittedRules, Node $node, FileIn
463471 }
464472
465473 /**
466- * Checks if the given user has admin (ACL management) rights for the group folder at the given path.
467- *
468- * Caches the result per folder ID for efficiency.
474+ * Checks if a user has admin (ACL management) rights for the group folder at the provided path.
475+ * Results are cached per folder for efficiency.
469476 *
470477 * @param IUser $user The user to check.
471478 * @param string $path The full path to a file or folder inside a group folder.
472- * @return bool True if the user can manage ACLs for the group folder at the given path, false otherwise.
479+ *
473480 * @throws \OCP\Files\NotFoundException If the path does not exist or is not part of a group folder.
474481 */
475482 private function isAdmin (IUser $ user , string $ path ): bool {
@@ -486,17 +493,12 @@ private function isAdmin(IUser $user, string $path): bool {
486493 }
487494
488495 /**
489- * Returns all parent directory paths for the given path, based solely on the path itself.
490- *
491- * The array is ordered from immediate parent upward, excluding the original path.
492- *
493- * Example:
494- * getParents('a/b/c.txt') returns ['a/b', 'a']
495- *
496- * Note: Callers should add contextual parents (such as mount points or absolute roots) if needed.
496+ * Returns all parent directory paths of the given path, from nearest to root.
497+ * Excludes the original path itself.
498+ * E.g.: 'a/b/c.txt' β ['a/b', 'a']
497499 *
498500 * @param string $path Path to a file or directory.
499- * @return string[] Parent directory paths, from closest to furthest.
501+ * @return list< string> Parent directory paths, from closest to furthest.
500502 */
501503 private function getParents (string $ path ): array {
502504 $ parents = [];
0 commit comments