Skip to content

Commit abe850f

Browse files
committed
Merge branch 'master' of github.com:chamilo/chamilo-lms
2 parents 2c449a9 + 1cc54ca commit abe850f

File tree

10 files changed

+161
-30
lines changed

10 files changed

+161
-30
lines changed

src/CoreBundle/Controller/Api/DownloadSelectedDocumentsAction.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@
77
namespace Chamilo\CoreBundle\Controller\Api;
88

99
use Chamilo\CoreBundle\Entity\ResourceNode;
10+
use Chamilo\CoreBundle\Exception\NotAllowedException;
11+
use Chamilo\CoreBundle\Helpers\CidReqHelper;
1012
use Chamilo\CoreBundle\Helpers\ResourceFileHelper;
1113
use Chamilo\CoreBundle\Repository\ResourceNodeRepository;
14+
use Chamilo\CoreBundle\Security\Authorization\Voter\CourseVoter;
15+
use Chamilo\CoreBundle\Security\Authorization\Voter\GroupVoter;
16+
use Chamilo\CoreBundle\Security\Authorization\Voter\ResourceFileVoter;
17+
use Chamilo\CoreBundle\Security\Authorization\Voter\SessionVoter;
1218
use Chamilo\CoreBundle\Traits\ControllerTrait;
1319
use Chamilo\CourseBundle\Repository\CDocumentRepository;
1420
use Exception;
21+
use Symfony\Bundle\SecurityBundle\Security;
1522
use Symfony\Component\HttpFoundation\Request;
1623
use Symfony\Component\HttpFoundation\Response;
1724
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
@@ -31,6 +38,8 @@ public function __construct(
3138
private readonly ResourceNodeRepository $resourceNodeRepository,
3239
private readonly CDocumentRepository $documentRepo,
3340
private readonly ResourceFileHelper $resourceFileHelper,
41+
private readonly Security $security,
42+
private readonly CidReqHelper $cidReqHelper,
3443
) {}
3544

3645
/**
@@ -48,7 +57,32 @@ public function __invoke(Request $request): Response
4857
return new Response('No items selected.', Response::HTTP_BAD_REQUEST);
4958
}
5059

51-
$documents = $this->documentRepo->findBy(['iid' => $documentIds]);
60+
if ($this->security->isGranted('ROLE_ADMIN')) {
61+
$documents = $this->documentRepo->findBy(['iid' => $documentIds]);
62+
} else {
63+
$course = $this->cidReqHelper->getCourseEntity();
64+
$session = $this->cidReqHelper->getSessionEntity();
65+
$group = $this->cidReqHelper->getGroupEntity();
66+
67+
if (!$course || !$this->security->isGranted(CourseVoter::VIEW, $course)) {
68+
throw new NotAllowedException("You're not allowed in this course");
69+
}
70+
71+
if ($session && !$this->security->isGranted(SessionVoter::VIEW, $session)) {
72+
throw new NotAllowedException("You're not allowed in this session");
73+
}
74+
75+
if ($group && !$this->security->isGranted(GroupVoter::VIEW, $group)) {
76+
throw new NotAllowedException("You're not allowed in this group");
77+
}
78+
79+
$qb = $this->documentRepo->getResourcesByCourse($course, $session, $group);
80+
$qb->andWhere(
81+
$qb->expr()->in('resource.iid', $documentIds)
82+
);
83+
84+
$documents = $qb->getQuery()->getResult();
85+
}
5286

5387
if (empty($documents)) {
5488
return new Response('No documents found.', Response::HTTP_NOT_FOUND);
@@ -120,6 +154,10 @@ private function addNodeToZip(ZipStream $zip, ResourceNode $node, string $curren
120154
$resourceFile = $this->resourceFileHelper->resolveResourceFileByAccessUrl($node);
121155

122156
if ($resourceFile) {
157+
if (!$this->security->isGranted(ResourceFileVoter::DOWNLOAD, $resourceFile)) {
158+
return;
159+
}
160+
123161
$fileName = $currentPath.$resourceFile->getOriginalName();
124162
$stream = $this->resourceNodeRepository->getResourceNodeFileStream($node, $resourceFile);
125163

src/CoreBundle/EventListener/CidReqListener.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ public function onKernelRequest(RequestEvent $event): void
171171
throw new AccessDeniedException($this->translator->trans("You're not allowed in this group"));
172172
}
173173

174+
$sessionHandler->set('group', $group);
174175
$sessionHandler->set('gid', $groupId);
175176
// @todo check if course has group
176177
/*if ($course->hasGroup($group)) {

src/CoreBundle/Helpers/CidReqHelper.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Chamilo\CoreBundle\Entity\Course;
1010
use Chamilo\CoreBundle\Entity\Session;
1111
use Chamilo\CoreBundle\EventListener\CidReqListener;
12+
use Chamilo\CourseBundle\Entity\CGroup;
1213
use Doctrine\ORM\EntityManagerInterface;
1314
use Symfony\Component\HttpFoundation\Request;
1415
use Symfony\Component\HttpFoundation\RequestStack;
@@ -71,6 +72,11 @@ public function getGroupId(): ?int
7172
return $session?->get('gid');
7273
}
7374

75+
public function getGroupEntity(): ?CGroup
76+
{
77+
return $this->getSessionHandler()->get('group');
78+
}
79+
7480
public function getDoctrineCourseEntity(): ?Course
7581
{
7682
$courseId = $this->getCourseId();

src/CoreBundle/Helpers/ResourceAclHelper.php

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
namespace Chamilo\CoreBundle\Helpers;
88

99
use Chamilo\CoreBundle\Entity\ResourceLink;
10+
use Chamilo\CoreBundle\Entity\ResourceRight;
11+
use Chamilo\CoreBundle\Entity\User;
12+
use Chamilo\CoreBundle\Security\Authorization\Voter\ResourceFileVoter;
1013
use Chamilo\CoreBundle\Security\Authorization\Voter\ResourceNodeVoter;
1114
use Laminas\Permissions\Acl\Acl;
1215
use Laminas\Permissions\Acl\Resource\GenericResource;
@@ -22,12 +25,13 @@ public function __construct(
2225
private Security $security,
2326
) { }
2427

25-
public function isAllowed(
26-
string $attribute,
28+
/**
29+
* @param iterable<int, ResourceRight> $rights
30+
*/
31+
private function init(
2732
ResourceLink $resourceLink,
2833
iterable $rights,
29-
bool $allowAnonsToView,
30-
): bool {
34+
): Acl {
3135
// Creating roles
3236
$anon = new GenericRole('IS_AUTHENTICATED_ANONYMOUSLY');
3337
$userRole = new GenericRole('ROLE_USER');
@@ -71,16 +75,20 @@ public function isAllowed(
7175
$acl->allow($right->getRole(), null, (string) $right->getMask());
7276
}
7377

74-
// Anons can see.
75-
if ($allowAnonsToView) {
76-
$acl->allow($anon, null, (string) ResourceNodeVoter::getReaderMask());
77-
}
78+
return $acl;
79+
}
7880

79-
// Asked mask
80-
$mask = new MaskBuilder();
81-
$mask->add($attribute);
81+
/**
82+
* @param iterable<int, ResourceRight> $rights
83+
*/
84+
public function isAllowed(
85+
string $attribute,
86+
ResourceLink $resourceLink,
87+
iterable $rights,
88+
): bool {
89+
$acl = $this->init($resourceLink, $rights);
8290

83-
$askedMask = (string) $mask->get();
91+
$askedMask = (string) self::getPermissionMask([$attribute]);
8492

8593
if ($this->security->getToken() instanceof NullToken) {
8694
return (bool) $acl->isAllowed('IS_AUTHENTICATED_ANONYMOUSLY', $resourceLink->getId(), $askedMask);
@@ -98,4 +106,15 @@ public function isAllowed(
98106

99107
return false;
100108
}
109+
110+
public static function getPermissionMask(array $attributes): int
111+
{
112+
$builder = new MaskBuilder();
113+
114+
foreach ($attributes as $attribute) {
115+
$builder->add($attribute);
116+
}
117+
118+
return $builder->get();
119+
}
101120
}

src/CoreBundle/Repository/ResourceRepository.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818
use Chamilo\CoreBundle\Entity\Session;
1919
use Chamilo\CoreBundle\Entity\User;
2020
use Chamilo\CoreBundle\Helpers\CreateUploadedFileHelper;
21+
use Chamilo\CoreBundle\Helpers\ResourceAclHelper;
2122
use Chamilo\CoreBundle\Security\Authorization\Voter\ResourceNodeVoter;
2223
use Chamilo\CoreBundle\Traits\NonResourceRepository;
2324
use Chamilo\CoreBundle\Traits\Repository\RepositoryQueryBuilderTrait;
2425
use Chamilo\CourseBundle\Entity\CGroup;
2526
use DateTime;
2627
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
28+
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryProxy;
2729
use Doctrine\Common\Collections\ArrayCollection;
2830
use Doctrine\DBAL\Types\Types;
2931
use Doctrine\ORM\QueryBuilder;
@@ -37,6 +39,9 @@
3739

3840
/**
3941
* Extends Resource EntityRepository.
42+
*
43+
* @template T of object
44+
* @template-extends ServiceEntityRepositoryProxy<T>
4045
*/
4146
abstract class ResourceRepository extends ServiceEntityRepository
4247
{
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
/* For licensing terms, see /license.txt */
4+
5+
declare(strict_types=1);
6+
7+
namespace Chamilo\CoreBundle\Security\Authorization\Voter;
8+
9+
use Chamilo\CoreBundle\Entity\ResourceFile;
10+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
11+
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
12+
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
13+
14+
/**
15+
* @extends Voter<'DOWNLOAD', ResourceFile>
16+
*/
17+
final class ResourceFileVoter extends Voter
18+
{
19+
public const DOWNLOAD = 'VIEW';
20+
21+
public function __construct(
22+
private readonly AccessDecisionManagerInterface $accessDecisionManager,
23+
) {}
24+
25+
protected function supports(string $attribute, mixed $subject): bool
26+
{
27+
$options = [
28+
self::DOWNLOAD,
29+
];
30+
31+
if (!\in_array($attribute, $options, true)) {
32+
return false;
33+
}
34+
35+
return $subject instanceof ResourceFile;
36+
}
37+
38+
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
39+
{
40+
$resourceFile = $subject;
41+
$resourceNode = $resourceFile->getResourceNode();
42+
43+
if ($this->accessDecisionManager->decide($token, ['ROLE_ADMIN'])) {
44+
return true;
45+
}
46+
47+
return match ($attribute) {
48+
self::DOWNLOAD => $this->accessDecisionManager->decide(
49+
$token,
50+
[ResourceNodeVoter::VIEW],
51+
$resourceNode
52+
),
53+
default => false,
54+
};
55+
}
56+
}

src/CoreBundle/Security/Authorization/Voter/ResourceNodeVoter.php

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,12 @@ public function __construct(
6060

6161
public static function getReaderMask(): int
6262
{
63-
$builder = (new MaskBuilder())
64-
->add(self::VIEW)
65-
;
66-
67-
return $builder->get();
63+
return ResourceAclHelper::getPermissionMask([self::VIEW]);
6864
}
6965

7066
public static function getEditorMask(): int
7167
{
72-
$builder = (new MaskBuilder())
73-
->add(self::VIEW)
74-
->add(self::EDIT)
75-
;
76-
77-
return $builder->get();
68+
return ResourceAclHelper::getPermissionMask([self::VIEW, self::EDIT]);
7869
}
7970

8071
protected function supports(string $attribute, $subject): bool
@@ -357,7 +348,6 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $
357348

358349
// Getting rights from the link
359350
$rightsFromResourceLink = $link->getResourceRights();
360-
$allowAnonsToView = false;
361351

362352
$rights = [];
363353
if ($rightsFromResourceLink->count() > 0) {
@@ -400,7 +390,6 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $
400390
if (ResourceLink::VISIBILITY_PUBLISHED === $link->getVisibility()
401391
&& $link->getCourse()->isPublic()
402392
) {
403-
$allowAnonsToView = true;
404393
$resourceRight = (new ResourceRight())
405394
->setMask($readerMask)
406395
->setRole('IS_AUTHENTICATED_ANONYMOUSLY')
@@ -454,7 +443,7 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $
454443
$rights[] = $resourceRight;
455444
}
456445

457-
return $this->resourceAclHelper->isAllowed($attribute, $link, $rights, $allowAnonsToView);
446+
return $this->resourceAclHelper->isAllowed($attribute, $link, $rights);
458447
}
459448

460449
/**

src/CourseBundle/Entity/CDocument.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use ApiPlatform\Metadata\GetCollection;
1717
use ApiPlatform\Metadata\Post;
1818
use ApiPlatform\Metadata\Put;
19+
use ApiPlatform\Metadata\QueryParameter;
1920
use ApiPlatform\OpenApi\Model\Operation;
2021
use ApiPlatform\OpenApi\Model\Parameter;
2122
use ApiPlatform\OpenApi\Model\RequestBody;
@@ -162,6 +163,20 @@
162163
new Post(
163164
uriTemplate: '/documents/download-selected',
164165
controller: DownloadSelectedDocumentsAction::class,
166+
parameters: [
167+
'cid' => new QueryParameter(
168+
schema: ['type' => 'integer'],
169+
description: 'Course identifier',
170+
),
171+
'sid' => new QueryParameter(
172+
schema: ['type' => 'integer'],
173+
description: 'Session identifier',
174+
),
175+
'gid' => new QueryParameter(
176+
schema: ['type' => 'integer'],
177+
description: 'Course grou identifier',
178+
),
179+
],
165180
openapi: new Operation(
166181
summary: 'Download selected documents as a ZIP file.',
167182
description: 'Streams a ZIP archive generated on-the-fly. The ZIP file includes folders and files selected.',

src/CourseBundle/Repository/CDocumentRepository.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
use Symfony\Component\HttpFoundation\File\UploadedFile;
2626
use Throwable;
2727

28+
/**
29+
* @extends ResourceRepository<CDocument>
30+
*/
2831
final class CDocumentRepository extends ResourceRepository
2932
{
3033
public function __construct(ManagerRegistry $registry)

tests/behat/README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@ java -jar /my-dir/selenium-server-standalone-3.1.0.jar
1414

1515
- Download the Chrome driver, unzip and copy into /usr/bin
1616

17-
Check the latest version at https://sites.google.com/a/chromium.org/chromedriver/downloads,
18-
then adapt the following command to the latest version. Use a version that matches your version of the Chrome browser.
17+
Check the latest `chromedriver` version at [https://googlechromelabs.github.io/chrome-for-testing/](https://googlechromelabs.github.io/chrome-for-testing/) that matches your configuration (linux64 for Ubuntu), then adapt the following command to the latest version. Use a version that matches your version of the Chrome browser.
1918

2019
```
21-
cd /tmp && wget https://chromedriver.storage.googleapis.com/2.35/chromedriver_linux64.zip && unzip chromedriver_linux64.zip && sudo mv chromedriver /usr/local/bin
20+
cd /tmp && wget https://storage.googleapis.com/chrome-for-testing-public/143.0.7499.40/linux64/chromedriver-linux64.zip && unzip chromedriver_linux64.zip && sudo mv chromedriver /usr/local/bin
2221
```
2322

2423
### Chamilo configuration

0 commit comments

Comments
 (0)