1 | <?php |
||||
2 | /** |
||||
3 | * @copyright Copyright (c) 2016 Morris Jobke <[email protected]> |
||||
4 | * |
||||
5 | * @license GNU AGPL version 3 or any later version |
||||
6 | * |
||||
7 | * This program is free software: you can redistribute it and/or modify |
||||
8 | * it under the terms of the GNU Affero General Public License as |
||||
9 | * published by the Free Software Foundation, either version 3 of the |
||||
10 | * License, or (at your option) any later version. |
||||
11 | * |
||||
12 | * This program is distributed in the hope that it will be useful, |
||||
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
15 | * GNU Affero General Public License for more details. |
||||
16 | * |
||||
17 | * You should have received a copy of the GNU Affero General Public License |
||||
18 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
19 | * |
||||
20 | */ |
||||
21 | |||||
22 | namespace OCA\FilesAccessControl; |
||||
23 | |||||
24 | use Exception; |
||||
25 | use OCA\WorkflowEngine\Entity\File; |
||||
0 ignored issues
–
show
|
|||||
26 | use OCP\EventDispatcher\Event; |
||||
27 | use OCP\Files\ForbiddenException; |
||||
28 | use OCP\Files\Storage\IStorage; |
||||
29 | use OCP\IL10N; |
||||
30 | use OCP\IURLGenerator; |
||||
31 | use OCP\WorkflowEngine\IComplexOperation; |
||||
32 | use OCP\WorkflowEngine\IManager; |
||||
33 | use OCP\WorkflowEngine\IRuleMatcher; |
||||
34 | use OCP\WorkflowEngine\ISpecificOperation; |
||||
35 | use ReflectionClass; |
||||
36 | use UnexpectedValueException; |
||||
37 | |||||
38 | class Operation implements IComplexOperation, ISpecificOperation { |
||||
39 | /** @var IManager */ |
||||
40 | protected $manager; |
||||
41 | |||||
42 | /** @var IL10N */ |
||||
43 | protected $l; |
||||
44 | |||||
45 | /** @var IURLGenerator */ |
||||
46 | protected $urlGenerator; |
||||
47 | |||||
48 | /** @var int */ |
||||
49 | protected $nestingLevel = 0; |
||||
50 | |||||
51 | /** |
||||
52 | * @param IManager $manager |
||||
53 | * @param IL10N $l |
||||
54 | */ |
||||
55 | public function __construct(IManager $manager, IL10N $l, IURLGenerator $urlGenerator) { |
||||
56 | $this->manager = $manager; |
||||
57 | $this->l = $l; |
||||
58 | $this->urlGenerator = $urlGenerator; |
||||
59 | } |
||||
60 | |||||
61 | /** |
||||
62 | * @param IStorage $storage |
||||
63 | * @param string $path |
||||
64 | * @param bool $isDir |
||||
65 | * @throws ForbiddenException |
||||
66 | */ |
||||
67 | public function checkFileAccess(IStorage $storage, string $path, bool $isDir = false): void { |
||||
68 | if (!$this->isBlockablePath($storage, $path) || $this->isCreatingSkeletonFiles() || $this->nestingLevel !== 0) { |
||||
69 | // Allow creating skeletons and theming |
||||
70 | // https://github.com/nextcloud/files_accesscontrol/issues/5 |
||||
71 | // https://github.com/nextcloud/files_accesscontrol/issues/12 |
||||
72 | return; |
||||
73 | } |
||||
74 | |||||
75 | $this->nestingLevel++; |
||||
76 | |||||
77 | $filePath = $this->translatePath($storage, $path); |
||||
78 | $ruleMatcher = $this->manager->getRuleMatcher(); |
||||
79 | $ruleMatcher->setFileInfo($storage, $filePath, $isDir); |
||||
80 | $ruleMatcher->setOperation($this); |
||||
81 | $match = $ruleMatcher->getFlows(); |
||||
82 | |||||
83 | $this->nestingLevel--; |
||||
84 | |||||
85 | if (!empty($match)) { |
||||
86 | // All Checks of one operation matched: prevent access |
||||
87 | throw new ForbiddenException('Access denied', false); |
||||
88 | } |
||||
89 | } |
||||
90 | |||||
91 | protected function isBlockablePath(IStorage $storage, string $path): bool { |
||||
92 | if (property_exists($storage, 'mountPoint')) { |
||||
93 | $hasMountPoint = $storage instanceof StorageWrapper; |
||||
94 | if (!$hasMountPoint) { |
||||
95 | $ref = new ReflectionClass($storage); |
||||
96 | $prop = $ref->getProperty('mountPoint'); |
||||
97 | $hasMountPoint = $prop->isPublic(); |
||||
98 | } |
||||
99 | |||||
100 | if ($hasMountPoint) { |
||||
101 | /** @var StorageWrapper $storage */ |
||||
102 | $fullPath = $storage->mountPoint . ltrim($path, '/'); |
||||
103 | } else { |
||||
104 | $fullPath = $path; |
||||
105 | } |
||||
106 | } else { |
||||
107 | $fullPath = $path; |
||||
108 | } |
||||
109 | |||||
110 | if (substr_count($fullPath, '/') < 3) { |
||||
111 | return false; |
||||
112 | } |
||||
113 | |||||
114 | // '', admin, 'files', 'path/to/file.txt' |
||||
115 | $segment = explode('/', $fullPath, 4); |
||||
116 | |||||
117 | if (isset($segment[2]) && $segment[1] === '__groupfolders' && $segment[2] === 'trash') { |
||||
118 | // Special case, a file was deleted inside a groupfolder |
||||
119 | return true; |
||||
120 | } |
||||
121 | |||||
122 | return isset($segment[2]) && in_array($segment[2], [ |
||||
123 | 'files', |
||||
124 | 'thumbnails', |
||||
125 | 'files_versions', |
||||
126 | ]); |
||||
127 | } |
||||
128 | |||||
129 | /** |
||||
130 | * For thumbnails and versions we want to check the tags of the original file |
||||
131 | */ |
||||
132 | protected function translatePath(IStorage $storage, string $path): string { |
||||
133 | if (substr_count($path, '/') < 1) { |
||||
134 | return $path; |
||||
135 | } |
||||
136 | |||||
137 | // 'files', 'path/to/file.txt' |
||||
138 | [$folder, $innerPath] = explode('/', $path, 2); |
||||
139 | |||||
140 | if ($folder === 'files_versions') { |
||||
141 | $innerPath = substr($innerPath, 0, strrpos($innerPath, '.v')); |
||||
142 | return 'files/' . $innerPath; |
||||
143 | } elseif ($folder === 'thumbnails') { |
||||
144 | [$fileId,] = explode('/', $innerPath, 2); |
||||
145 | $innerPath = $storage->getCache()->getPathById($fileId); |
||||
0 ignored issues
–
show
$fileId of type string is incompatible with the type integer expected by parameter $id of OCP\Files\Cache\ICache::getPathById() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
146 | |||||
147 | if ($innerPath !== null) { |
||||
148 | return 'files/' . $innerPath; |
||||
149 | } |
||||
150 | } |
||||
151 | |||||
152 | return $path; |
||||
153 | } |
||||
154 | |||||
155 | /** |
||||
156 | * Check if we are in the LoginController and if so, ignore the firewall |
||||
157 | */ |
||||
158 | protected function isCreatingSkeletonFiles(): bool { |
||||
159 | $exception = new Exception(); |
||||
160 | $trace = $exception->getTrace(); |
||||
161 | |||||
162 | foreach ($trace as $step) { |
||||
163 | if (isset($step['class']) && $step['class'] === 'OC\Core\Controller\LoginController' && |
||||
164 | isset($step['function']) && $step['function'] === 'tryLogin') { |
||||
165 | return true; |
||||
166 | } |
||||
167 | } |
||||
168 | |||||
169 | return false; |
||||
170 | } |
||||
171 | |||||
172 | /** |
||||
173 | * @param string $name |
||||
174 | * @param array[] $checks |
||||
175 | * @param string $operation |
||||
176 | * @throws UnexpectedValueException |
||||
177 | */ |
||||
178 | public function validateOperation(string $name, array $checks, string $operation): void { |
||||
179 | if (empty($checks)) { |
||||
180 | throw new UnexpectedValueException($this->l->t('No rule given')); |
||||
181 | } |
||||
182 | } |
||||
183 | |||||
184 | /** |
||||
185 | * returns a translated name to be presented in the web interface |
||||
186 | * |
||||
187 | * Example: "Automated tagging" (en), "Aŭtomata etikedado" (eo) |
||||
188 | * |
||||
189 | * @since 18.0.0 |
||||
190 | */ |
||||
191 | public function getDisplayName(): string { |
||||
192 | return $this->l->t('Block access to a file'); |
||||
193 | } |
||||
194 | |||||
195 | /** |
||||
196 | * returns a translated, descriptive text to be presented in the web interface. |
||||
197 | * |
||||
198 | * It should be short and precise. |
||||
199 | * |
||||
200 | * Example: "Tag based automatic deletion of files after a given time." (en) |
||||
201 | * |
||||
202 | * @since 18.0.0 |
||||
203 | */ |
||||
204 | public function getDescription(): string { |
||||
205 | return ''; |
||||
206 | } |
||||
207 | |||||
208 | /** |
||||
209 | * returns the URL to the icon of the operator for display in the web interface. |
||||
210 | * |
||||
211 | * Usually, the implementation would utilize the `imagePath()` method of the |
||||
212 | * `\OCP\IURLGenerator` instance and simply return its result. |
||||
213 | * |
||||
214 | * Example implementation: return $this->urlGenerator->imagePath('myApp', 'cat.svg'); |
||||
215 | * |
||||
216 | * @since 18.0.0 |
||||
217 | */ |
||||
218 | public function getIcon(): string { |
||||
219 | return $this->urlGenerator->imagePath('files_accesscontrol', 'app.svg'); |
||||
220 | } |
||||
221 | |||||
222 | /** |
||||
223 | * returns whether the operation can be used in the requested scope. |
||||
224 | * |
||||
225 | * Scope IDs are defined as constants in OCP\WorkflowEngine\IManager. At |
||||
226 | * time of writing these are SCOPE_ADMIN and SCOPE_USER. |
||||
227 | * |
||||
228 | * For possibly unknown future scopes the recommended behaviour is: if |
||||
229 | * user scope is permitted, the default behaviour should return `true`, |
||||
230 | * otherwise `false`. |
||||
231 | * |
||||
232 | * @since 18.0.0 |
||||
233 | */ |
||||
234 | public function isAvailableForScope(int $scope): bool { |
||||
235 | return $scope === IManager::SCOPE_ADMIN; |
||||
236 | } |
||||
237 | |||||
238 | /** |
||||
239 | * returns the id of the entity the operator is designed for |
||||
240 | * |
||||
241 | * Example: 'WorkflowEngine_Entity_File' |
||||
242 | * |
||||
243 | * @since 18.0.0 |
||||
244 | */ |
||||
245 | public function getEntityId(): string { |
||||
246 | return File::class; |
||||
247 | } |
||||
248 | |||||
249 | /** |
||||
250 | * As IComplexOperation chooses the triggering events itself, a hint has |
||||
251 | * to be shown to the user so make clear when this operation is becoming |
||||
252 | * active. This method returns such a translated string. |
||||
253 | * |
||||
254 | * Example: "When a file is accessed" (en) |
||||
255 | * |
||||
256 | * @since 18.0.0 |
||||
257 | */ |
||||
258 | public function getTriggerHint(): string { |
||||
259 | return $this->l->t('File is accessed'); |
||||
260 | } |
||||
261 | |||||
262 | public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatcher): void { |
||||
263 | // Noop |
||||
264 | } |
||||
265 | } |
||||
266 |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"]
, you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths