Release 2.58 (#5952)
* bump version * fix formatting locale reset after embedded controller sub-requests (#5944) * fix GHSA-c6w6-57jj-62vh * fix GHSA-m492-gv72-xvxj * fix GHSA-jr9p-4h4j-6c58 * make sure to only use JS logic to call API endpoints * fixes GHSA-r8vr-m544-qh4h * make sure to only use JS logic to call API endpoints * fix GHSA-rw46-qg69-vg6h * fix GHSA-pj8j-p4g4-4vw8 - prevent kimai from rendering images via markdown * fix GHSA-pj8j-p4g4-4vw8 - use a safe network client to prevent SSRF via images * fix GHSA-xv4r-4885-gwpg * fix GHSA-pgcc-vfmc-7cw5 - move GET routes to API with POST method to prevent CSRF * fix tooltip survives page reload * updated wizard images * split wizard and password reset subscriber into two classes * relax upper php limit * added zizmor workflow scans and apply findings * user permissions <name>_other_profile now respect teams * move all linting steps to new job * updated docker image version names * use .env.local for storing APP_SECRET * improve build order and use given tag as ref for checkout, not default main branch * improved APP_SECRET handling, see entrypoint.sh * use local code for building the image for more flexibility, added dockerignore
This commit is contained in:
@@ -1528,6 +1528,72 @@ class TimesheetControllerTest extends APIControllerBaseTestCase
|
||||
$this->assertEntityNotFoundForPatch(User::ROLE_ADMIN, '/api/timesheets/11/duplicate', []);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// GHSA-c6w6-57jj-62vh — restart/duplicate after project access revocation.
|
||||
//
|
||||
// "restart" and "duplicate" derive a NEW timesheet from a historical
|
||||
// entry the user still owns. Once the user's team access to the underlying
|
||||
// project/activity is revoked, neither operation may create a new record
|
||||
// under it. The data write itself is already blocked by
|
||||
// TimesheetTeamAccessValidator (since 2.57); these tests additionally pin
|
||||
// that the TimesheetVoter denies the request at the authorization layer —
|
||||
// a clean 403, not an incidental 400 from downstream validation.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public function testRestartAndDuplicateDeniedAfterProjectAccessRevoked(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$em = $this->getEntityManager();
|
||||
$owner = $this->getUserByRole(User::ROLE_USER);
|
||||
|
||||
// The customer is restricted to a team the user is NOT a member of:
|
||||
// the user's access to this project/activity has been revoked, but
|
||||
// their historical timesheet still references it.
|
||||
$revokedTeam = new Team('GHSA-c6w6 team without access');
|
||||
$em->persist($revokedTeam);
|
||||
|
||||
$timesheet = $this->persistRestrictedTimesheet($owner, [$revokedTeam], running: false);
|
||||
$id = $timesheet->getId();
|
||||
self::assertIsInt($id);
|
||||
|
||||
$before = $this->getEntityManager()->getRepository(Timesheet::class)->count([]);
|
||||
|
||||
// PATCH + GET .../restart
|
||||
$this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH');
|
||||
$this->assertApiResponseAccessDenied($client->getResponse());
|
||||
|
||||
// PATCH .../duplicate
|
||||
$this->request($client, '/api/timesheets/' . $id . '/duplicate', 'PATCH');
|
||||
$this->assertApiResponseAccessDenied($client->getResponse());
|
||||
|
||||
// No new record may have been persisted under the revoked project.
|
||||
$after = $this->getEntityManager()->getRepository(Timesheet::class)->count([]);
|
||||
self::assertSame($before, $after, 'restart/duplicate leaked through and created a new timesheet under the revoked project');
|
||||
}
|
||||
|
||||
public function testRestartAndDuplicateAllowedWhenUserStillHasProjectAccess(): void
|
||||
{
|
||||
// Positive control: as long as the user still has team access to the
|
||||
// project/activity, restart and duplicate keep working.
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$em = $this->getEntityManager();
|
||||
$owner = $this->getUserByRole(User::ROLE_USER);
|
||||
|
||||
$team = new Team('GHSA-c6w6 team with access');
|
||||
$team->addUser($owner);
|
||||
$em->persist($team);
|
||||
|
||||
$timesheet = $this->persistRestrictedTimesheet($owner, [$team], running: false);
|
||||
$id = $timesheet->getId();
|
||||
self::assertIsInt($id);
|
||||
|
||||
$this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH');
|
||||
self::assertTrue($client->getResponse()->isSuccessful(), 'restart must succeed while the user still has project access');
|
||||
|
||||
$this->request($client, '/api/timesheets/' . $id . '/duplicate', 'PATCH');
|
||||
self::assertTrue($client->getResponse()->isSuccessful(), 'duplicate must succeed while the user still has project access');
|
||||
}
|
||||
|
||||
public function testExportAction(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
@@ -1695,17 +1761,17 @@ class TimesheetControllerTest extends APIControllerBaseTestCase
|
||||
$this->request($client, '/api/timesheets/' . $id, 'PATCH', [], $patch);
|
||||
$this->assertApiResponseAccessDenied($client->getResponse());
|
||||
|
||||
// 3) PATCH /api/timesheets/{id}/stop and 4) GET .../stop
|
||||
// 3) PATCH /api/timesheets/{id}/stop is access-denied; 4) GET .../stop is no longer routable
|
||||
$this->request($client, '/api/timesheets/' . $id . '/stop', 'PATCH');
|
||||
$this->assertApiResponseAccessDenied($client->getResponse());
|
||||
$this->request($client, '/api/timesheets/' . $id . '/stop', 'GET');
|
||||
$this->assertApiResponseAccessDenied($client->getResponse());
|
||||
self::assertEquals(Response::HTTP_METHOD_NOT_ALLOWED, $client->getResponse()->getStatusCode());
|
||||
|
||||
// 5) PATCH /api/timesheets/{id}/restart and 6) GET .../restart
|
||||
// 5) PATCH /api/timesheets/{id}/restart is access-denied; 6) GET .../restart is no longer routable
|
||||
$this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH');
|
||||
$this->assertApiResponseAccessDenied($client->getResponse());
|
||||
$this->request($client, '/api/timesheets/' . $id . '/restart', 'GET');
|
||||
$this->assertApiResponseAccessDenied($client->getResponse());
|
||||
self::assertEquals(Response::HTTP_METHOD_NOT_ALLOWED, $client->getResponse()->getStatusCode());
|
||||
|
||||
// 7) PATCH /api/timesheets/{id}/duplicate
|
||||
$this->request($client, '/api/timesheets/' . $id . '/duplicate', 'PATCH');
|
||||
@@ -1931,7 +1997,7 @@ class TimesheetControllerTest extends APIControllerBaseTestCase
|
||||
$this->assertApiResponseAccessDenied($client->getResponse());
|
||||
|
||||
$this->request($client, '/api/timesheets/' . $id . '/stop', 'GET');
|
||||
$this->assertApiResponseAccessDenied($client->getResponse());
|
||||
self::assertEquals(Response::HTTP_METHOD_NOT_ALLOWED, $client->getResponse()->getStatusCode());
|
||||
|
||||
// Confirm side-effect-free: timesheet must still be running.
|
||||
$em->clear();
|
||||
|
||||
Reference in New Issue
Block a user