Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 24 additions & 14 deletions app/V1Module/presenters/GroupsPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
use App\Model\View\ShadowAssignmentViewFactory;
use App\Model\View\GroupViewFactory;
use App\Model\View\UserViewFactory;
use App\Model\GroupExamLockType;
use App\Security\ACL\IAssignmentPermissions;
use App\Security\ACL\IAssignmentSolutionPermissions;
use App\Security\ACL\IShadowAssignmentPermissions;
Expand Down Expand Up @@ -608,7 +609,7 @@ public function checkSetExamPeriod(string $id)
"When the exam ends (unix ts in the future, no more than a day after 'begin').",
required: true,
)]
#[Post("strict", new VBool(), "Whether locked users are prevented from accessing other groups.", required: false)]
#[Post("type", new VString(), "Lock type ('visible', 'reviewed', 'accepted', 'restricted').", required: false)]
#[Path("id", new VUuid(), "An identifier of the updated group", required: true)]
#[ResponseFormat(GroupFormat::class)]
public function actionSetExamPeriod(string $id)
Expand All @@ -618,19 +619,28 @@ public function actionSetExamPeriod(string $id)
$req = $this->getRequest();
$beginTs = (int)$req->getPost("begin");
$endTs = (int)$req->getPost("end");
$strict = $req->getPost("strict") !== null
? filter_var($req->getPost("strict"), FILTER_VALIDATE_BOOLEAN) : null;
$now = (new DateTime())->getTimestamp();
$nowTolerance = 60; // 60s is a tolerance when comparing with "now"

if ($strict === null) {
$typeStr = $req->getPost("type");
$type = null;
if ($typeStr !== null) {
$type = GroupExamLockType::tryFrom($typeStr);
if ($type === null) {
throw new InvalidApiArgumentException(
'type',
"Invalid lock type. Allowed values are: " . implode(", ", GroupExamLockType::values())
);
}
} else {
if ($group->hasExamPeriodSet()) {
$strict = $group->isExamLockStrict(); // flag is not present -> is not changing
$type = $group->getExamLockType(); // type is not present -> is not changing
} else {
throw new BadRequestException("The strict flag must be present when new exam is being set.");
throw new BadRequestException("The lock type must be present when new exam is being set.");
}
}

$now = (new DateTime())->getTimestamp();
$nowTolerance = 60; // 60s is a tolerance when comparing with "now"

// beginning must be in the future (or must not be modified)
if ((!$group->hasExamPeriodSet() || $beginTs) && $beginTs < $now - $nowTolerance) {
throw new BadRequestException("The exam must be set in the future.");
Expand All @@ -655,14 +665,14 @@ public function actionSetExamPeriod(string $id)

if ($group->hasExamPeriodSet()) {
if ($group->getExamBegin()->getTimestamp() <= $now) { // ... already begun
if ($strict !== $group->isExamLockStrict()) {
throw new BadRequestException("The strict flag cannot be changed once the exam begins.");
if ($type !== $group->getExamLockType()) {
throw new BadRequestException("The lock type cannot be changed once the exam begins.");
}

// the exam already begun, we need to fix any group-locked users
foreach ($group->getStudents() as $student) {
if ($student->getGroupLock()?->getId() === $id) {
$student->setGroupLock($group, $end, $strict);
$student->setGroupLock($group, $end, $type);
if ($student->isIpLocked()) {
$student->setIpLock($student->getIpLockRaw(), $end);
}
Expand Down Expand Up @@ -696,11 +706,11 @@ public function actionSetExamPeriod(string $id)

$exam = $this->groupExams->findPendingForGroup($group);
if ($exam) {
$exam->update($begin, $end, $strict);
$exam->update($begin, $end, $type);
$this->groupExams->persist($exam, false);
}

$group->setExamPeriod($begin, $end, $strict);
$group->setExamPeriod($begin, $end, $type);
$this->groups->persist($group);

$this->sendSuccessResponse($this->groupViewFactory->getGroup($group));
Expand Down Expand Up @@ -1298,7 +1308,7 @@ public function actionLockStudent(string $id, string $userId)

$expiration = $group->getExamEnd();
$user->setIpLock($this->getHttpRequest()->getRemoteAddress(), $expiration);
$user->setGroupLock($group, $expiration, $group->isExamLockStrict());
$user->setGroupLock($group, $expiration, $group->getExamLockType());
$this->users->persist($user, false);

// make sure the locking is also logged
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Model\Entity\AssignmentSolution;
use App\Model\Entity\GroupMembership;
use App\Model\GroupExamLockType;
use App\Security\Identity;

class AssignmentSolutionPermissionPolicy extends BasePermissionPolicy implements IPermissionPolicy
Expand Down Expand Up @@ -107,17 +108,38 @@ public function userIsNotLockedElsewhere(Identity $identity, AssignmentSolution
}

/**
* Current user is either not locked at all, or locked to this group, or the current lock is not strict.
* Current user is either not locked at all, or locked to this group (where the solution is),
* or the current lock type allows (read-only) access to this solution.
*/
public function userIsNotLockedElsewhereStrictly(Identity $identity, AssignmentSolution $solution): bool
public function userGroupLockTypeAllowsReadAccess(Identity $identity, AssignmentSolution $solution): bool
{
$user = $identity->getUserData();
$group = $solution->getAssignment()?->getGroup();
if ($user === null || $group === null) {
return false;
}

return !$user->isGroupLocked() || $user->getGroupLock()->getId() === $group->getId()
|| !$user->isGroupLockStrict();
if (!$user->isGroupLocked() || $user->getGroupLock()->getId() === $group->getId()) {
return true;
}

$lockType = $user->getGroupLockType();
if ($lockType === null || $lockType === GroupExamLockType::Visible) {
return true;
}

if ($lockType === GroupExamLockType::Restricted) {
return false; // a shortcut (false is also at the end)
}

if ($lockType === GroupExamLockType::Accepted) {
return $solution->isAccepted();
}

if ($lockType === GroupExamLockType::Reviewed) {
return $solution->isAccepted() || $solution->isReviewed();
}

return false;
}
}
2 changes: 2 additions & 0 deletions app/commands/security/ListExamEvents.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ protected function getLockData(GroupExamLock $lock): array
'remote_addr' => $lock->getRemoteAddr(),
'exam_begin_at' => $lock->getGroupExam()->getBegin()->getTimestamp(),
'exam_end_at' => $lock->getGroupExam()->getEnd()->getTimestamp(),
'lock_type' => $lock->getGroupExam()->getLockType()->value,
// DEPRECATED, use lock_type instead
'lock_strict' => $lock->getGroupExam()->isLockStrict(),
];
}
Expand Down
6 changes: 3 additions & 3 deletions app/config/permissions.neon
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,7 @@ permissions:
- viewReview
conditions:
- assignmentSolution.isAuthor
- assignmentSolution.userIsNotLockedElsewhereStrictly
- assignmentSolution.userGroupLockTypeAllowsReadAccess

- allow: true
role: student
Expand All @@ -633,7 +633,7 @@ permissions:
conditions:
- assignmentSolution.areEvaluationDetailsPublic
- assignmentSolution.isAuthor
- assignmentSolution.userIsNotLockedElsewhereStrictly
- assignmentSolution.userGroupLockTypeAllowsReadAccess

- allow: true
role: student
Expand All @@ -644,7 +644,7 @@ permissions:
- assignmentSolution.areEvaluationDetailsPublic
- assignmentSolution.areMeasuredValuesPublic
- assignmentSolution.isAuthor
- assignmentSolution.userIsNotLockedElsewhereStrictly
- assignmentSolution.userGroupLockTypeAllowsReadAccess

- allow: true
role: supervisor-student
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use App\Helpers\MetaFormats\Validators\VInt;
use App\Helpers\MetaFormats\Validators\VTimestamp;
use App\Helpers\MetaFormats\Validators\VUuid;
use App\Helpers\MetaFormats\Validators\VString;

/**
* Nested Format definition used by the GroupFormat.
Expand Down Expand Up @@ -71,8 +72,12 @@ class GroupPrivateDataFormat extends MetaFormat
#[FPost(new VTimestamp(), "The time when the exam ends if there is an exam scheduled", required: false)]
public ?int $examEnd;

#[FPost(new VBool(), "Whether the scheduled exam requires a strict access lock", required: false)]
public ?bool $examLockStrict;
#[FPost(
new VString(),
"Type (restriction level) of the exam lock ('restricted', 'accepted', 'reviewed', 'visible')",
required: false
)]
public ?string $examLockType;

#[FPost(new VArray(), "All past exams (with at least one student locked)")]
public array $exams;
Expand Down
27 changes: 18 additions & 9 deletions app/model/entity/Group.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace App\Model\Entity;

use DateTime;
use App\Model\GroupExamLockType;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ReadableCollection;
use Doctrine\Common\Collections\ArrayCollection;
Expand All @@ -11,6 +11,7 @@
use Gedmo\Mapping\Annotation as Gedmo;
use LogicException;
use InvalidArgumentException;
use DateTime;

/**
* @ORM\Entity
Expand Down Expand Up @@ -231,22 +232,25 @@ public function isDirectlyArchived(): bool
* @ORM\Column(type="datetime", nullable=true)
* When an exam in this groups begins. In the exam period, a user must lock in a group to be allowed
* submitting solutions. This is completely independent of the isExam flag.
* @var DateTime|null
*/
protected $examBegin = null;

/**
* @ORM\Column(type="datetime", nullable=true)
* When an exam in this groups ends. In the exam period, a user must lock in a group to be allowed
* submitting solutions. This is completely independent of the isExam flag.
* @var DateTime|null
*/
protected $examEnd = null;

/**
* @ORM\Column(type="boolean")
* Whether the group-lock for the exam should be strict
* @ORM\Column(type="string")
* The type of lock for the exam.
* (under strict lock, the user cannot read data from other groups).
* @var string
*/
protected $examLockStrict = false;
protected $examLockType = GroupExamLockType::Visible->value;

/**
* @var Collection
Expand All @@ -260,9 +264,9 @@ public function isDirectlyArchived(): bool
* Switch the group into an exam group by setting the begin and end dates of the exam.
* @param DateTime $begin when the exam starts
* @param DateTime $end when the exam ends
* @param bool $strict if true, locked users cannot access other groups (for reading)
* @param GroupExamLockType $type the type of lock for the exam
*/
public function setExamPeriod(DateTime $begin, DateTime $end, bool $strict = false): void
public function setExamPeriod(DateTime $begin, DateTime $end, GroupExamLockType $type): void
{
// asserts
if ($begin >= $end) {
Expand All @@ -275,7 +279,7 @@ public function setExamPeriod(DateTime $begin, DateTime $end, bool $strict = fal

$this->examBegin = $begin;
$this->examEnd = $end;
$this->examLockStrict = $strict;
$this->examLockType = $type->value;
$this->isOrganizational = false;
}

Expand All @@ -286,7 +290,7 @@ public function removeExamPeriod(): void
{
$this->examBegin = null;
$this->examEnd = null;
$this->examLockStrict = false;
$this->examLockType = GroupExamLockType::Visible->value;
}

/**
Expand All @@ -301,7 +305,12 @@ public function hasExamPeriodSet(?DateTime $at = null): bool

public function isExamLockStrict(): bool
{
return $this->examLockStrict;
return $this->examLockType === GroupExamLockType::Restricted->value;
}

public function getExamLockType(): GroupExamLockType
{
return GroupExamLockType::from($this->examLockType);
}

/**
Expand Down
Loading
Loading