diff --git a/ci/apiv2/test_cracker.py b/ci/apiv2/test_cracker.py index e89544668..cf54f26c7 100644 --- a/ci/apiv2/test_cracker.py +++ b/ci/apiv2/test_cracker.py @@ -136,7 +136,7 @@ def test_create_with_both_sources_rejects(self): def test_create_with_missing_source_data_rejects(self): obj = Cracker(crackerBinaryTypeId=1, version='7.2.7', binaryName='cracker', - sourceType='inline') + sourceType='inline', accessGroupId=1) with self.assertRaises(HashtopolisError) as e: obj.save() self.assertEqual(400, e.exception.status_code) diff --git a/ci/apiv2/test_logentry.py b/ci/apiv2/test_logentry.py index 3b5649c7a..6bdf8ef21 100644 --- a/ci/apiv2/test_logentry.py +++ b/ci/apiv2/test_logentry.py @@ -6,7 +6,13 @@ class LogEntryTest(BaseTest): model_class = LogEntry def test_get_one(self): - obj = LogEntry.objects.get(pk=1) + # the id of the oldest entry cannot be assumed, the server deletes the + # oldest log entries once the configured limit is exceeded + entries = LogEntry.objects.all() + if len(entries) == 0: + self.skipTest('no log entries exist yet on this database') + obj = LogEntry.objects.get(pk=entries[0].id) self.assertIsNotNone(obj) + self.assertEqual(entries[0].id, obj.id) # TODO: Create event which generate logenties and check if logentry is created diff --git a/ci/apiv2/test_taskwrapper.py b/ci/apiv2/test_taskwrapper.py index 5615c391c..00d3f3a04 100644 --- a/ci/apiv2/test_taskwrapper.py +++ b/ci/apiv2/test_taskwrapper.py @@ -60,7 +60,8 @@ def test_helper_create_supertask_generic_cracker(self): crackerBinaryTypeId=crackertype.id, version='1.2.3', downloadUrl='https://example.org/generic-1.2.3.gz', - binaryName='generic-x64') + binaryName='generic-x64', + accessGroupId=1) cracker.save() self.delete_after_test(cracker) hashlist = self.create_hashlist() diff --git a/ci/apiv2/testfiles/cracker/create_cracker_001.json b/ci/apiv2/testfiles/cracker/create_cracker_001.json index c90c25570..724423e43 100644 --- a/ci/apiv2/testfiles/cracker/create_cracker_001.json +++ b/ci/apiv2/testfiles/cracker/create_cracker_001.json @@ -2,6 +2,6 @@ "crackerBinaryTypeId": 1, "version": "0.0.1", "downloadUrl": "https://example.org/files/cracker-0.0.1.7z", - "binaryName": "cracker" + "binaryName": "cracker", + "accessGroupId": 1 } - diff --git a/ci/apiv2/testfiles/cracker/create_cracker_002.json b/ci/apiv2/testfiles/cracker/create_cracker_002.json index 2155e3591..f380568cb 100644 --- a/ci/apiv2/testfiles/cracker/create_cracker_002.json +++ b/ci/apiv2/testfiles/cracker/create_cracker_002.json @@ -1,6 +1,6 @@ { "crackerBinaryTypeId": 1, "version": "0.0.1", - "downloadUrl": "https://example.org/files/cracker-0.0.1.7z" + "downloadUrl": "https://example.org/files/cracker-0.0.1.7z", + "accessGroupId": 1 } - diff --git a/ci/phpunit/TestBase.php b/ci/phpunit/TestBase.php index 4340a5f5a..cf1f4c824 100644 --- a/ci/phpunit/TestBase.php +++ b/ci/phpunit/TestBase.php @@ -210,10 +210,10 @@ protected function createCrackerBinaryType(): CrackerBinaryType { /** * @throws Exception */ - protected function createCrackerBinary(CrackerBinaryType $crackerBinaryType): CrackerBinary { + protected function createCrackerBinary(CrackerBinaryType $crackerBinaryType, int $accessGroupId = 1): CrackerBinary { $crackerBinary = $this->createDatabaseObject( Factory::getCrackerBinaryFactory(), - new CrackerBinary(null, $crackerBinaryType->getId(), '1.0.' . uniqid(), 'https://example.invalid/' . uniqid(), 'binary_' . uniqid(), null) + new CrackerBinary(null, $crackerBinaryType->getId(), '1.0.' . uniqid(), 'https://example.invalid/' . uniqid(), 'binary_' . uniqid(), null, $accessGroupId) ); $this->assertTrue($crackerBinary instanceof CrackerBinary); return $crackerBinary; diff --git a/ci/phpunit/dba/AbstractModelFactoryTest.php b/ci/phpunit/dba/AbstractModelFactoryTest.php index 18adb7feb..f6ee9166a 100644 --- a/ci/phpunit/dba/AbstractModelFactoryTest.php +++ b/ci/phpunit/dba/AbstractModelFactoryTest.php @@ -1681,7 +1681,7 @@ private function setUpHealthCheck(): array { $crackerBinaryType = new CrackerBinaryType(null, '', 0); $crackerBinaryType = $this->createDatabaseObject(Factory::getCrackerBinaryTypeFactory(), $crackerBinaryType); - $crackerBinary = new CrackerBinary(null, $crackerBinaryType->getId(), '', '', '', null); + $crackerBinary = new CrackerBinary(null, $crackerBinaryType->getId(), '', '', '', null, 1); $crackerBinary = $this->createDatabaseObject(Factory::getCrackerBinaryFactory(), $crackerBinary); $healthCheck = new HealthCheck(null, 0, 0, 0, $hashType->getId(), $crackerBinary->getId(), 0, ''); diff --git a/ci/phpunit/dba/MassUpdateSetTest.php b/ci/phpunit/dba/MassUpdateSetTest.php index 5bbb199ca..2833ae74b 100644 --- a/ci/phpunit/dba/MassUpdateSetTest.php +++ b/ci/phpunit/dba/MassUpdateSetTest.php @@ -159,7 +159,7 @@ public function testMassSingleUpdateWithMappedColumn(): void { $agent = $this->createDatabaseObject(Factory::getAgentFactory(), new Agent(null, '', '', 0, '', '', 0, 0, 0, '', '', 0, '', null, 0, '')); $hashType = $this->createDatabaseObject(Factory::getHashTypeFactory(), new HashType(null, $prefix . '_ht', 0, 0)); $cbt = $this->createDatabaseObject(Factory::getCrackerBinaryTypeFactory(), new CrackerBinaryType(null, '', 0)); - $cb = $this->createDatabaseObject(Factory::getCrackerBinaryFactory(), new CrackerBinary(null, $cbt->getId(), '', '', '', null)); + $cb = $this->createDatabaseObject(Factory::getCrackerBinaryFactory(), new CrackerBinary(null, $cbt->getId(), '', '', '', null, 1)); $healthCheck = $this->createDatabaseObject(Factory::getHealthCheckFactory(), new HealthCheck(null, 0, 0, 0, $hashType->getId(), $cb->getId(), 0, '')); $hca1 = $this->createDatabaseObject(Factory::getHealthCheckAgentFactory(), new HealthCheckAgent(null, $healthCheck->getId(), $agent->getId(), 0, 0, 0, 0, 100, '')); diff --git a/ci/phpunit/downloadapi/DownloadAppTest.php b/ci/phpunit/downloadapi/DownloadAppTest.php index 357685285..a905a7d7c 100644 --- a/ci/phpunit/downloadapi/DownloadAppTest.php +++ b/ci/phpunit/downloadapi/DownloadAppTest.php @@ -4,6 +4,7 @@ use Hashtopolis\dba\Factory; use Hashtopolis\dba\models\Agent; +use Hashtopolis\dba\models\AccessGroupAgent; use Hashtopolis\dba\models\CrackerBinary; use Hashtopolis\dba\models\CrackerBinaryType; use Hashtopolis\inc\defines\DDirectories; @@ -43,20 +44,24 @@ protected function setUp(): void { ); $this->externalBinary = $this->createDatabaseObject( Factory::getCrackerBinaryFactory(), - new CrackerBinary(null, $this->type->getId(), '1.0.0', 'http://example.com/hc.7z', 'testcracker', null) + new CrackerBinary(null, $this->type->getId(), '1.0.0', 'http://example.com/hc.7z', 'testcracker', null, 1) ); // create a locally stored binary through the import source $this->agentToken = 'dl-test-' . uniqid(); - $this->createDatabaseObject( + $agent = $this->createDatabaseObject( Factory::getAgentFactory(), new Agent(null, 'download-test-agent', '', 0, '', '', 0, 0, 0, $this->agentToken, '', 0, '', null, 0, '') ); + $this->createDatabaseObject( + Factory::getAccessGroupAgentFactory(), + new AccessGroupAgent(null, 1, $agent->getId()) + ); $importName = 'download-test-' . uniqid() . '.7z'; $this->archiveContent = self::SEVEN_ZIP_MAGIC . 'download-test-content'; file_put_contents(self::getImportPath() . $importName, $this->archiveContent); - $this->localBinary = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'import', $importName); + $this->localBinary = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'import', $importName, 1); $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $this->localBinary); if (isset($_SERVER['HTTP_RANGE'])) { diff --git a/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json b/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json index 7fae5a60e..7a8dfe7f9 100644 --- a/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json +++ b/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json @@ -1699,7 +1699,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -1724,6 +1725,10 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } } } @@ -2081,7 +2086,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -2106,6 +2112,10 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } } } @@ -2463,7 +2473,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -2488,6 +2499,10 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } } } @@ -2897,7 +2912,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -2922,6 +2938,10 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } } } diff --git a/ci/phpunit/inc/UtilTest.php b/ci/phpunit/inc/UtilTest.php index 97dd3d118..2b4a72bf5 100644 --- a/ci/phpunit/inc/UtilTest.php +++ b/ci/phpunit/inc/UtilTest.php @@ -974,7 +974,8 @@ public function testCheckOrCreateInitialObjectCrackerBinary(): void { 'crackerBinaryTypeId' => $typeId, 'version' => '7.0.0', 'downloadUrl' => 'https://example.com/test.7z', - 'binaryName' => 'testHashcat' + 'binaryName' => 'testHashcat', + 'accessGroupId' => 1 ]; Util::checkOrCreateInitialObject(Factory::getCrackerBinaryFactory(), $data); $obj = Factory::getCrackerBinaryFactory()->get($id); diff --git a/ci/phpunit/inc/utils/AccessGroupUtilsTest.php b/ci/phpunit/inc/utils/AccessGroupUtilsTest.php index e91f3b6a2..e829564e2 100644 --- a/ci/phpunit/inc/utils/AccessGroupUtilsTest.php +++ b/ci/phpunit/inc/utils/AccessGroupUtilsTest.php @@ -7,6 +7,7 @@ use Hashtopolis\dba\models\AccessGroupAgent; use Hashtopolis\dba\models\AccessGroupUser; use Hashtopolis\dba\models\Agent; +use Hashtopolis\dba\models\CrackerBinary; use Hashtopolis\dba\models\Chunk; use Hashtopolis\dba\models\File; use Hashtopolis\dba\models\Hashlist; @@ -18,6 +19,7 @@ use Hashtopolis\inc\defines\DHashcatStatus; use Hashtopolis\inc\defines\DLimits; use Hashtopolis\inc\HTException; +use Hashtopolis\inc\utils\CrackerUtils; use Hashtopolis\TestBase; use Override; @@ -262,12 +264,15 @@ public function testDeleteGroupReassignsDependentEntitiesToDefaultGroup(): void $hashlist = $this->createHashlist($groupToDelete, $hashType); $taskWrapper = $this->createTaskWrapper($groupToDelete, $hashlist); $file = $this->createFile($groupToDelete); + $crackerBinary = CrackerUtils::createBinary('1.0.0', 'delgroup-cracker', 'http://example.com/hc.7z', 1, $groupToDelete->getId()); + $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $crackerBinary); AccessGroupUtils::deleteGroup($groupToDelete->getId()); $updatedHashlist = Factory::getHashlistFactory()->get($hashlist->getId()); $updatedTaskWrapper = Factory::getTaskWrapperFactory()->get($taskWrapper->getId()); $updatedFile = Factory::getFileFactory()->get($file->getId()); + $updatedCrackerBinary = Factory::getCrackerBinaryFactory()->get($crackerBinary->getId()); $deletedGroup = Factory::getAccessGroupFactory()->get($groupToDelete->getId()); $remainingUsers = AccessGroupUtils::getUsers($groupToDelete->getId()); $remainingAgents = AccessGroupUtils::getAgents($groupToDelete->getId()); @@ -278,6 +283,8 @@ public function testDeleteGroupReassignsDependentEntitiesToDefaultGroup(): void $this->assertSame($defaultGroup->getId(), $updatedTaskWrapper->getAccessGroupId()); $this->assertInstanceOf(File::class, $updatedFile); $this->assertSame($defaultGroup->getId(), $updatedFile->getAccessGroupId()); + $this->assertInstanceOf(CrackerBinary::class, $updatedCrackerBinary); + $this->assertSame($defaultGroup->getId(), $updatedCrackerBinary->getAccessGroupId()); $this->assertNull($deletedGroup); $this->assertSame([], $remainingUsers); $this->assertSame([], $remainingAgents); diff --git a/ci/phpunit/inc/utils/AccessUtilsTest.php b/ci/phpunit/inc/utils/AccessUtilsTest.php index d0ea3f8e5..03a7821cf 100644 --- a/ci/phpunit/inc/utils/AccessUtilsTest.php +++ b/ci/phpunit/inc/utils/AccessUtilsTest.php @@ -414,7 +414,7 @@ public function testAgentCanAccessTaskWhenWrapperHashlistAndFilesAreAllowed(): v $hashlist = $this->createHashlist($group, $hashType); $taskWrapper = $this->createTaskWrapper($group, $hashlist); $crackerBinaryType = $this->createCrackerBinaryType(); - $crackerBinary = $this->createCrackerBinary($crackerBinaryType); + $crackerBinary = $this->createCrackerBinary($crackerBinaryType, $group->getId()); $task = $this->createTask($taskWrapper, $crackerBinary, $crackerBinaryType); $file = $this->createFile($group); @@ -426,4 +426,4 @@ public function testAgentCanAccessTaskWhenWrapperHashlistAndFilesAreAllowed(): v $this->assertTrue(AccessUtils::agentCanAccessTask($agent, $task)); } -} \ No newline at end of file +} diff --git a/ci/phpunit/inc/utils/CrackerBinaryUtilsTest.php b/ci/phpunit/inc/utils/CrackerBinaryUtilsTest.php index d835092bf..e42ae4ea2 100644 --- a/ci/phpunit/inc/utils/CrackerBinaryUtilsTest.php +++ b/ci/phpunit/inc/utils/CrackerBinaryUtilsTest.php @@ -6,8 +6,12 @@ use Hashtopolis\dba\Factory; use Hashtopolis\dba\models\CrackerBinary; use Hashtopolis\dba\models\CrackerBinaryType; +use Hashtopolis\dba\models\AccessGroupUser; use Hashtopolis\inc\HTException; +use Hashtopolis\inc\utils\AccessGroupUtils; +use Hashtopolis\inc\utils\AccessUtils; use Hashtopolis\inc\utils\CrackerBinaryUtils; +use Hashtopolis\inc\utils\CrackerUtils; use Hashtopolis\TestBase; @@ -36,7 +40,7 @@ protected function setUp(): void { private function addBinary(string $version): AbstractModel { return $this->createDatabaseObject( Factory::getCrackerBinaryFactory(), - new CrackerBinary(null, $this->type->getId(), $version, 'http://example.com', 'testcracker', null) + new CrackerBinary(null, $this->type->getId(), $version, 'http://example.com', 'testcracker', null, 1) ); } @@ -75,4 +79,39 @@ public function testGetNewestVersionOutOfOrderInsertStillReturnsHighest(): void $result = CrackerBinaryUtils::getNewestVersion($this->type->getId()); $this->assertSame($newest->getId(), $result->getId()); } + + // Verifies that getNewestVersion() only considers binaries of access groups the + // user is a member of — binaries of one type can be in different groups. + public function testGetNewestVersionRespectsUserGroups(): void { + $group = $this->createAccessGroup('ag-newestversion'); + $user = $this->createUser('newestversion-user'); + + // 2.0.0 is in the default group, 1.0.0 in the group of the user + $highVersion = $this->addBinary('2.0.0'); + $lowVersion = CrackerUtils::createBinary('1.0.0', 'testcracker', 'http://example.com', $this->type->getId(), $group->getId()); + $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $lowVersion); + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $group->getId(), $user->getId()) + ); + // createUser made the user a member of the default group too, remove it so + // only the binary in the test group is accessible + AccessGroupUtils::removeUser($user->getId(), AccessUtils::getOrCreateDefaultAccessGroup()->getId()); + + // without a user all binaries are considered + $this->assertSame($highVersion->getId(), CrackerBinaryUtils::getNewestVersion($this->type->getId())->getId()); + // the user can only access 1.0.0, so it is picked over the newer 2.0.0 + $this->assertSame($lowVersion->getId(), CrackerBinaryUtils::getNewestVersion($this->type->getId(), $user)->getId()); + + // a user without access to any binary of the type gets no version + $otherUser = $this->createUser('newestversion-other-user'); + AccessGroupUtils::removeUser($otherUser->getId(), AccessUtils::getOrCreateDefaultAccessGroup()->getId()); + try { + CrackerBinaryUtils::getNewestVersion($this->type->getId(), $otherUser); + $this->fail('Expected HTException when the user has no accessible binary'); + } + catch (HTException $e) { + $this->assertStringContainsString('No binary versions available', $e->getMessage()); + } + } } diff --git a/ci/phpunit/inc/utils/CrackerUtilsTest.php b/ci/phpunit/inc/utils/CrackerUtilsTest.php index 42d2272c9..d1e0db7c7 100644 --- a/ci/phpunit/inc/utils/CrackerUtilsTest.php +++ b/ci/phpunit/inc/utils/CrackerUtilsTest.php @@ -7,6 +7,7 @@ use Hashtopolis\dba\QueryFilter; use Hashtopolis\dba\models\CrackerBinary; use Hashtopolis\dba\models\CrackerBinaryType; +use Hashtopolis\dba\models\AccessGroupUser; use Hashtopolis\inc\Util; use Hashtopolis\inc\defines\DDirectories; use Hashtopolis\inc\apiv2\error\HttpConflict; @@ -39,7 +40,7 @@ protected function setUp(): void { ); $this->binary = $this->createDatabaseObject( Factory::getCrackerBinaryFactory(), - new CrackerBinary(null, $this->type->getId(), '1.0.0', 'http://example.com', 'testcracker', null) + new CrackerBinary(null, $this->type->getId(), '1.0.0', 'http://example.com', 'testcracker', null, 1) ); } @@ -89,13 +90,13 @@ public function testCreateBinaryTypeDuplicateNameThrowsHttpConflict(): void { // empty. Uses a valid type ID so the method reaches the field validation. public function testCreateBinaryEmptyVersionThrowsHttpError(): void { $this->expectException(HttpError::class); - CrackerUtils::createBinary('', 'testcracker', 'http://example.com', $this->type->getId()); + CrackerUtils::createBinary('', 'testcracker', 'http://example.com', $this->type->getId(), 1); } // Verifies the full happy path: createBinary() creates and returns a new // CrackerBinary when all fields are valid. public function testCreateBinaryValidInputCreatesBinary(): void { - $b = CrackerUtils::createBinary('9.9.9', 'newcracker', 'http://example.com/dl', $this->type->getId()); + $b = CrackerUtils::createBinary('9.9.9', 'newcracker', 'http://example.com/dl', $this->type->getId(), 1); $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $b); $this->assertSame('9.9.9', $b->getVersion()); } @@ -118,7 +119,7 @@ public function testCreateBinaryFromUploadImportSource(): void { $name = 'test-archive-' . uniqid() . '.7z'; $content = self::SEVEN_ZIP_MAGIC . 'test-content'; file_put_contents($this->getImportPath() . $name, $content); - $b = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'import', $name); + $b = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'import', $name, 1); $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $b); $this->assertEquals('test-crackerutils-type-7.2.7.7z', $b->getFilename()); @@ -138,7 +139,7 @@ public function testCreateBinaryFromUploadImportSource(): void { // stores the base64 decoded archive in the crackers directory. public function testCreateBinaryFromUploadInlineSource(): void { $content = self::SEVEN_ZIP_MAGIC . 'inline-content'; - $b = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'inline', base64_encode($content)); + $b = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'inline', base64_encode($content), 1); $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $b); $this->assertEquals('test-crackerutils-type-7.2.7.7z', $b->getFilename()); @@ -157,7 +158,7 @@ public function testCreateBinaryFromUploadSanitizesFilename(): void { Factory::getCrackerBinaryTypeFactory(), new CrackerBinaryType(null, 'weird cracker name!', 1) ); - $b = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $type->getId(), 'inline', base64_encode(self::SEVEN_ZIP_MAGIC)); + $b = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $type->getId(), 'inline', base64_encode(self::SEVEN_ZIP_MAGIC), 1); $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $b); $this->assertEquals('weird-cracker-name--7.2.7.7z', $b->getFilename()); @@ -167,32 +168,32 @@ public function testCreateBinaryFromUploadSanitizesFilename(): void { // Verifies that createBinaryFromUpload() rejects an unsupported sourceType. public function testCreateBinaryFromUploadInvalidSourceTypeThrowsHttpError(): void { $this->expectException(HttpError::class); - CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'bogus', 'data'); + CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'bogus', 'data', 1); } // Verifies that createBinaryFromUpload() rejects an empty version. public function testCreateBinaryFromUploadEmptyVersionThrowsHttpError(): void { $this->expectException(HttpError::class); - CrackerUtils::createBinaryFromUpload('', 'testcracker', $this->type->getId(), 'inline', base64_encode(self::SEVEN_ZIP_MAGIC)); + CrackerUtils::createBinaryFromUpload('', 'testcracker', $this->type->getId(), 'inline', base64_encode(self::SEVEN_ZIP_MAGIC), 1); } // Verifies that createBinaryFromUpload() rejects missing sourceData. public function testCreateBinaryFromUploadEmptySourceDataThrowsHttpError(): void { $this->expectException(HttpError::class); - CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'inline', ''); + CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'inline', '', 1); } // Verifies that createBinaryFromUpload() rejects sourceData which is not valid base64. public function testCreateBinaryFromUploadInvalidBase64ThrowsHttpError(): void { $this->expectException(HttpError::class); - CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'inline', '!!!no-base64!!!'); + CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'inline', '!!!no-base64!!!', 1); } // Verifies that createBinaryFromUpload() only allows http and https urls, so no // local files or stream wrappers can be fetched by the server. public function testCreateBinaryFromUploadUrlSchemeThrowsHttpError(): void { $this->expectException(HttpError::class); - CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'url', 'file:///etc/passwd'); + CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'url', 'file:///etc/passwd', 1); } // Verifies that a non-7z archive is rejected and the import file is restored and @@ -203,7 +204,7 @@ public function testCreateBinaryFromUploadImportNot7zRollsBack(): void { $countBefore = $this->countBinariesOfType($this->type->getId()); try { - CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'import', $name); + CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'import', $name, 1); $this->fail('Expected HttpError for a non-7z archive'); } catch (HttpError $e) { @@ -222,7 +223,7 @@ public function testCreateBinaryFromUploadImportFileMissingRollsBack(): void { $countBefore = $this->countBinariesOfType($this->type->getId()); try { - CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'import', 'does-not-exist-' . uniqid() . '.7z'); + CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'import', 'does-not-exist-' . uniqid() . '.7z', 1); $this->fail('Expected HttpError for a missing import file'); } catch (HttpError $e) { @@ -236,7 +237,7 @@ public function testCreateBinaryFromUploadImportFileMissingRollsBack(): void { public function testDeleteBinaryRemovesLocalArchive(): void { $name = 'test-archive-' . uniqid() . '.7z'; file_put_contents($this->getImportPath() . $name, self::SEVEN_ZIP_MAGIC . 'to-be-deleted'); - $b = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'import', $name); + $b = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'import', $name, 1); $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $b); $archive = CrackerUtils::getCrackersPath() . $b->getId() . '_' . $b->getFilename(); $this->assertFileExists($archive); @@ -256,7 +257,7 @@ public function testDeleteBinaryTypeRemovesLocalArchives(): void { ); $name = 'test-archive-' . uniqid() . '.7z'; file_put_contents($this->getImportPath() . $name, self::SEVEN_ZIP_MAGIC . 'to-be-deleted'); - $b = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $type->getId(), 'import', $name); + $b = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $type->getId(), 'import', $name, 1); $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $b); $archive = CrackerUtils::getCrackersPath() . $b->getId() . '_' . $b->getFilename(); $this->assertFileExists($archive); @@ -271,7 +272,7 @@ public function testDeleteBinaryTypeRemovesLocalArchives(): void { public function testUpdateBinaryRejectsUrlChangeForLocalBinary(): void { $name = 'test-archive-' . uniqid() . '.7z'; file_put_contents($this->getImportPath() . $name, self::SEVEN_ZIP_MAGIC . 'local'); - $b = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'import', $name); + $b = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'import', $name, 1); $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $b); try { @@ -292,7 +293,7 @@ public function testUpdateBinaryRejectsUrlChangeForLocalBinary(): void { public function testUpdateBinaryAllowsUnchangedUrlForLocalBinary(): void { $name = 'test-archive-' . uniqid() . '.7z'; file_put_contents($this->getImportPath() . $name, self::SEVEN_ZIP_MAGIC . 'local'); - $b = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'import', $name); + $b = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $this->type->getId(), 'import', $name, 1); $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $b); CrackerUtils::updateBinary('8.0.0', 'testcracker', $b->getDownloadUrl(), $b->getId()); @@ -310,4 +311,125 @@ public function testUpdateBinaryChangesUrlForExternalBinary(): void { $this->assertEquals('http://changed.example.com/hc.7z', $reloaded->getDownloadUrl()); $this->assertEquals('2.0.0', $reloaded->getVersion()); } + + // Verifies that binaries can only be created in access groups the user is a + // member of. + public function testCreateBinaryRequiresGroupMembership(): void { + $group = $this->createAccessGroup('ag-crackerutils-member'); + $user = $this->createUser('crackerutils-member-user'); + + try { + CrackerUtils::createBinary('1.0.0', 'testcracker', 'http://example.com/hc.7z', $this->type->getId(), $group->getId(), $user); + $this->fail('Expected HttpError when the user is not a member of the access group'); + } + catch (HttpError $e) { + $this->assertStringContainsString('no rights', $e->getMessage()); + } + + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $group->getId(), $user->getId()) + ); + $binary = CrackerUtils::createBinary('1.0.0', 'testcracker', 'http://example.com/hc.7z', $this->type->getId(), $group->getId(), $user); + $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $binary); + $this->assertEquals($group->getId(), $binary->getAccessGroupId()); + } + + // Verifies that uploads can only be created in access groups the user is a + // member of. + public function testCreateBinaryFromUploadRequiresGroupMembership(): void { + $group = $this->createAccessGroup('ag-crackerutils-upload'); + $user = $this->createUser('crackerutils-upload-user'); + + try { + CrackerUtils::createBinaryFromUpload('1.0.0', 'testcracker', $this->type->getId(), 'inline', + base64_encode(self::SEVEN_ZIP_MAGIC . 'content'), $group->getId(), $user); + $this->fail('Expected HttpError when the user is not a member of the access group'); + } + catch (HttpError $e) { + $this->assertStringContainsString('no rights', $e->getMessage()); + } + $this->assertEmpty(glob(CrackerUtils::getCrackersPath() . '*_test-crackerutils-type-1.0.0.7z')); + + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $group->getId(), $user->getId()) + ); + $binary = CrackerUtils::createBinaryFromUpload('1.0.0', 'testcracker', $this->type->getId(), 'inline', + base64_encode(self::SEVEN_ZIP_MAGIC . 'content'), $group->getId(), $user); + $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $binary); + $this->assertEquals($group->getId(), $binary->getAccessGroupId()); + unlink(CrackerUtils::getCrackersPath() . $binary->getId() . '_' . $binary->getFilename()); + } + + // Verifies that creation with a group that does not exist is rejected. + public function testCreateBinaryRejectsInvalidGroup(): void { + try { + CrackerUtils::createBinary('1.0.0', 'testcracker', 'http://example.com/hc.7z', $this->type->getId(), 99999999, $this->adminUser); + $this->fail('Expected HttpError for a non existing access group'); + } + catch (HttpError $e) { + $this->assertStringContainsString('Invalid access group', $e->getMessage()); + } + } + + // Verifies that moving a binary to another group requires membership of the + // current and of the new group. + public function testChangeAccessGroupRequiresMembershipOfBothGroups(): void { + $group1 = $this->createAccessGroup('ag-crackerutils-move-1'); + $group2 = $this->createAccessGroup('ag-crackerutils-move-2'); + $user = $this->createUser('crackerutils-move-user'); + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $group1->getId(), $user->getId()) + ); + $binary = CrackerUtils::createBinary('1.0.0', 'testcracker', 'http://example.com/hc.7z', $this->type->getId(), $group1->getId()); + $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $binary); + + // the user is not a member of the new group + try { + CrackerUtils::changeAccessGroup($binary->getId(), $group2->getId(), $user); + $this->fail('Expected HttpError when the user is not a member of the new group'); + } + catch (HttpError $e) { + $this->assertStringContainsString('No access to this group', $e->getMessage()); + } + + $otherUser = $this->createUser('crackerutils-move-user-2'); + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $group2->getId(), $otherUser->getId()) + ); + // the other user is not a member of the current group of the binary + try { + CrackerUtils::changeAccessGroup($binary->getId(), $group2->getId(), $otherUser); + $this->fail('Expected HttpError when the user is not a member of the current group'); + } + catch (HttpError $e) { + $this->assertStringContainsString('No access to this group', $e->getMessage()); + } + + $this->assertEquals($group1->getId(), Factory::getCrackerBinaryFactory()->get($binary->getId())->getAccessGroupId()); + } + + // Verifies that a binary can be moved to another group the user is a member of. + public function testChangeAccessGroupMovesBinary(): void { + $group1 = $this->createAccessGroup('ag-crackerutils-move-ok-1'); + $group2 = $this->createAccessGroup('ag-crackerutils-move-ok-2'); + $user = $this->createUser('crackerutils-move-ok-user'); + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $group1->getId(), $user->getId()) + ); + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $group2->getId(), $user->getId()) + ); + $binary = CrackerUtils::createBinary('1.0.0', 'testcracker', 'http://example.com/hc.7z', $this->type->getId(), $group1->getId()); + $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $binary); + + CrackerUtils::changeAccessGroup($binary->getId(), $group2->getId(), $user); + + $this->assertEquals($group2->getId(), Factory::getCrackerBinaryFactory()->get($binary->getId())->getAccessGroupId()); + } } diff --git a/openapi.json b/openapi.json index bce35d5d3..c49c1bf57 100644 --- a/openapi.json +++ b/openapi.json @@ -7792,14 +7792,15 @@ "type": "string", "enum": [ "crackerBinaryType", + "accessGroup", "tasks" ] } }, - "description": "Relationships to include in the response, comma seperated. Possible options: crackerBinaryType, tasks", + "description": "Relationships to include in the response, comma seperated. Possible options: crackerBinaryType, accessGroup, tasks", "example": [ "crackerBinaryType", - "tasks" + "accessGroup" ] } ], @@ -8652,14 +8653,15 @@ "type": "string", "enum": [ "crackerBinaryType", + "accessGroup", "tasks" ] } }, - "description": "Relationships to include in the response, comma seperated. Possible options: crackerBinaryType, tasks", + "description": "Relationships to include in the response, comma seperated. Possible options: crackerBinaryType, accessGroup, tasks", "example": [ "crackerBinaryType", - "tasks" + "accessGroup" ] } ], @@ -41904,12 +41906,17 @@ }, "binaryName": { "type": "string" + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } }, "required": [ "crackerBinaryTypeId", "version", - "binaryName" + "binaryName", + "accessGroupId" ] } } @@ -41936,6 +41943,9 @@ "attributes": { "type": "object", "properties": { + "accessGroupId": { + "type": "integer" + }, "binaryName": { "type": "string" }, @@ -41981,6 +41991,9 @@ "attributes": { "type": "object", "properties": { + "accessGroupId": { + "type": "integer" + }, "binaryName": { "type": "string" }, @@ -42094,7 +42107,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -42119,6 +42133,10 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } } }, @@ -42137,10 +42155,60 @@ "relationships": { "type": "object", "required": [ + "accessGroup", "crackerBinaryType", "tasks" ], "properties": { + "accessGroup": { + "type": "object", + "required": [ + "links" + ], + "properties": { + "links": { + "type": "object", + "required": [ + "self", + "related" + ], + "properties": { + "self": { + "type": "string", + "default": "/api/v2/ui/crackers/relationships/accessGroup" + }, + "related": { + "type": "string", + "default": "/api/v2/ui/crackers/accessGroup" + } + } + }, + "data": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "id" + ], + "properties": { + "type": { + "type": "string", + "const": "accessGroup" + }, + "id": { + "type": "integer", + "example": 1 + } + } + }, + { + "type": "null" + } + ] + } + } + }, "crackerBinaryType": { "type": "object", "required": [ @@ -42279,6 +42347,35 @@ } } }, + { + "type": "object", + "required": [ + "id", + "type", + "attributes" + ], + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "type": { + "type": "string", + "const": "accessGroup" + }, + "attributes": { + "type": "object", + "required": [ + "groupName" + ], + "properties": { + "groupName": { + "type": "string" + } + } + } + } + }, { "type": "object", "required": [ @@ -42480,7 +42577,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -42505,6 +42603,10 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } } }, @@ -42523,10 +42625,60 @@ "relationships": { "type": "object", "required": [ + "accessGroup", "crackerBinaryType", "tasks" ], "properties": { + "accessGroup": { + "type": "object", + "required": [ + "links" + ], + "properties": { + "links": { + "type": "object", + "required": [ + "self", + "related" + ], + "properties": { + "self": { + "type": "string", + "default": "/api/v2/ui/crackers/relationships/accessGroup" + }, + "related": { + "type": "string", + "default": "/api/v2/ui/crackers/accessGroup" + } + } + }, + "data": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "id" + ], + "properties": { + "type": { + "type": "string", + "const": "accessGroup" + }, + "id": { + "type": "integer", + "example": 1 + } + } + }, + { + "type": "null" + } + ] + } + } + }, "crackerBinaryType": { "type": "object", "required": [ @@ -42665,6 +42817,35 @@ } } }, + { + "type": "object", + "required": [ + "id", + "type", + "attributes" + ], + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "type": { + "type": "string", + "const": "accessGroup" + }, + "attributes": { + "type": "object", + "required": [ + "groupName" + ], + "properties": { + "groupName": { + "type": "string" + } + } + } + } + }, { "type": "object", "required": [ @@ -42917,7 +43098,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -42942,6 +43124,10 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } } }, @@ -42960,10 +43146,60 @@ "relationships": { "type": "object", "required": [ + "accessGroup", "crackerBinaryType", "tasks" ], "properties": { + "accessGroup": { + "type": "object", + "required": [ + "links" + ], + "properties": { + "links": { + "type": "object", + "required": [ + "self", + "related" + ], + "properties": { + "self": { + "type": "string", + "default": "/api/v2/ui/crackers/relationships/accessGroup" + }, + "related": { + "type": "string", + "default": "/api/v2/ui/crackers/accessGroup" + } + } + }, + "data": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "id" + ], + "properties": { + "type": { + "type": "string", + "const": "accessGroup" + }, + "id": { + "type": "integer", + "example": 1 + } + } + }, + { + "type": "null" + } + ] + } + } + }, "crackerBinaryType": { "type": "object", "required": [ @@ -43103,6 +43339,35 @@ } } }, + { + "type": "object", + "required": [ + "id", + "type", + "attributes" + ], + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "type": { + "type": "string", + "const": "accessGroup" + }, + "attributes": { + "type": "object", + "required": [ + "groupName" + ], + "properties": { + "groupName": { + "type": "string" + } + } + } + } + }, { "type": "object", "required": [ @@ -43704,7 +43969,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -43729,6 +43995,10 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } } } @@ -44086,7 +44356,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -44111,6 +44382,10 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } } } @@ -44520,7 +44795,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -44545,6 +44821,10 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } } } @@ -54375,7 +54655,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -54400,6 +54681,10 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } } } @@ -54841,7 +55126,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -54866,6 +55152,10 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } } } @@ -55359,7 +55649,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -55384,6 +55675,10 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } } } @@ -62425,7 +62720,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -62450,6 +62746,10 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } } } @@ -63400,7 +63700,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -63425,6 +63726,10 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } } } @@ -64375,7 +64680,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -64400,6 +64706,10 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } } } @@ -65402,7 +65712,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -65427,6 +65738,10 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer", + "description": "Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups." } } } diff --git a/src/dba/models/CrackerBinary.php b/src/dba/models/CrackerBinary.php index 55c01cfb5..012325e2d 100644 --- a/src/dba/models/CrackerBinary.php +++ b/src/dba/models/CrackerBinary.php @@ -11,14 +11,16 @@ class CrackerBinary extends AbstractModel { private ?string $downloadUrl; private ?string $binaryName; private ?string $filename; + private ?int $accessGroupId; - function __construct(?int $crackerBinaryId, ?int $crackerBinaryTypeId, ?string $version, ?string $downloadUrl, ?string $binaryName, ?string $filename) { + function __construct(?int $crackerBinaryId, ?int $crackerBinaryTypeId, ?string $version, ?string $downloadUrl, ?string $binaryName, ?string $filename, ?int $accessGroupId) { $this->crackerBinaryId = $crackerBinaryId; $this->crackerBinaryTypeId = $crackerBinaryTypeId; $this->version = $version; $this->downloadUrl = $downloadUrl; $this->binaryName = $binaryName; $this->filename = $filename; + $this->accessGroupId = $accessGroupId; } function getKeyValueDict(): array { @@ -29,6 +31,7 @@ function getKeyValueDict(): array { $dict['downloadUrl'] = $this->downloadUrl; $dict['binaryName'] = $this->binaryName; $dict['filename'] = $this->filename; + $dict['accessGroupId'] = $this->accessGroupId; return $dict; } @@ -41,6 +44,7 @@ static function getFeatures(): array { $dict['downloadUrl'] = ['read_only' => False, "type" => "str(255)", "subtype" => "unset", "choices" => "unset", "null" => True, "pk" => False, "protected" => False, "private" => False, "alias" => "downloadUrl", "public" => False, "dba_mapping" => False]; $dict['binaryName'] = ['read_only' => False, "type" => "str(50)", "subtype" => "unset", "choices" => "unset", "null" => False, "pk" => False, "protected" => False, "private" => False, "alias" => "binaryName", "public" => False, "dba_mapping" => False]; $dict['filename'] = ['read_only' => True, "type" => "str(100)", "subtype" => "unset", "choices" => "unset", "null" => True, "pk" => False, "protected" => True, "private" => False, "alias" => "filename", "public" => False, "dba_mapping" => False]; + $dict['accessGroupId'] = ['read_only' => False, "type" => "int", "subtype" => "unset", "choices" => "unset", "null" => False, "pk" => False, "protected" => False, "private" => False, "alias" => "accessGroupId", "public" => False, "dba_mapping" => False]; return $dict; } @@ -109,12 +113,21 @@ function setFilename(?string $filename): void { $this->filename = $filename; } + function getAccessGroupId(): ?int { + return $this->accessGroupId; + } + + function setAccessGroupId(?int $accessGroupId): void { + $this->accessGroupId = $accessGroupId; + } + const CRACKER_BINARY_ID = "crackerBinaryId"; const CRACKER_BINARY_TYPE_ID = "crackerBinaryTypeId"; const VERSION = "version"; const DOWNLOAD_URL = "downloadUrl"; const BINARY_NAME = "binaryName"; const FILENAME = "filename"; + const ACCESS_GROUP_ID = "accessGroupId"; const PERM_CREATE = "permCrackerBinaryCreate"; const PERM_READ = "permCrackerBinaryRead"; diff --git a/src/dba/models/CrackerBinaryFactory.php b/src/dba/models/CrackerBinaryFactory.php index 459d29922..8428804c2 100644 --- a/src/dba/models/CrackerBinaryFactory.php +++ b/src/dba/models/CrackerBinaryFactory.php @@ -32,7 +32,7 @@ function getCacheValidTime(): int { * @return CrackerBinary */ function getNullObject(): CrackerBinary { - return new CrackerBinary(-1, null, null, null, null, null); + return new CrackerBinary(-1, null, null, null, null, null, null); } /** @@ -45,6 +45,6 @@ function createObjectFromDict(array $dict): CrackerBinary { $conv[strtolower($key)] = $val; } $dict = $conv; - return new CrackerBinary($dict['crackerbinaryid'], $dict['crackerbinarytypeid'], $dict['version'], $dict['downloadurl'], $dict['binaryname'], $dict['filename']); + return new CrackerBinary($dict['crackerbinaryid'], $dict['crackerbinarytypeid'], $dict['version'], $dict['downloadurl'], $dict['binaryname'], $dict['filename'], $dict['accessgroupid']); } } diff --git a/src/dba/models/generator.php b/src/dba/models/generator.php index 0220810b7..3c63b63e6 100644 --- a/src/dba/models/generator.php +++ b/src/dba/models/generator.php @@ -288,6 +288,9 @@ ['name' => 'binaryName', 'read_only' => False, 'type' => 'str(50)'], // archive filename of a server-hosted binary; NULL means the binary is downloaded from downloadUrl ['name' => 'filename', 'read_only' => True, 'null' => True, 'type' => 'str(100)', 'protected' => True], + // access group the binary belongs to, required on creation and patchable with + // membership validation of the current and the new group + ['name' => 'accessGroupId', 'read_only' => False, 'type' => 'int', 'relation' => 'AccessGroup'], ], ]; $CONF['CrackerBinaryType'] = [ diff --git a/src/inc/agentapi/model/DownloadBinaryAction.php b/src/inc/agentapi/model/DownloadBinaryAction.php index 884b3f4b2..fb44ddac5 100644 --- a/src/inc/agentapi/model/DownloadBinaryAction.php +++ b/src/inc/agentapi/model/DownloadBinaryAction.php @@ -17,6 +17,7 @@ use Hashtopolis\inc\defines\DServerLog; use Hashtopolis\inc\SConfig; use Hashtopolis\inc\Util; +use Hashtopolis\inc\utils\AccessUtils; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface as Request; use Slim\Psr7\Response; @@ -66,6 +67,12 @@ public function __invoke(Request $request, Response $response): ResponseInterfac if ($crackerBinary === null) { return $this->error($response, PActions::DOWNLOAD_BINARY, 'Invalid cracker binary type id!'); } + if (!in_array( + $crackerBinary->getAccessGroupId(), + Util::arrayOfIds(AccessUtils::getAccessGroupsOfAgent($agent)) + )) { + return $this->error($response, PActions::DOWNLOAD_BINARY, 'No access to this cracker binary!'); + } $crackerBinaryType = Factory::getCrackerBinaryTypeFactory()->get($crackerBinary->getCrackerBinaryTypeId()); DServerLog::log(DServerLog::TRACE, 'Agent ' . $agent->getId() . ' downloaded cracker binary ' . $crackerBinary->getId()); $ext = Util::getFileExtension($agent->getOs()); diff --git a/src/inc/apiv2/helper/CreateSupertaskHelperAPI.php b/src/inc/apiv2/helper/CreateSupertaskHelperAPI.php index 824b83db9..cf8d19039 100644 --- a/src/inc/apiv2/helper/CreateSupertaskHelperAPI.php +++ b/src/inc/apiv2/helper/CreateSupertaskHelperAPI.php @@ -67,7 +67,8 @@ public function actionPost($data): AbstractModel|array|null { SupertaskUtils::runSupertask( $supertaskTemplate->getId(), $hashlist->getId(), - $crackerBinary->getId() + $crackerBinary->getId(), + $this->getCurrentUser() ); /* Quick to retrieve newly created TaskWrapper */ diff --git a/src/inc/apiv2/model/CrackerBinaryAPI.php b/src/inc/apiv2/model/CrackerBinaryAPI.php index a804b80dd..903af021c 100644 --- a/src/inc/apiv2/model/CrackerBinaryAPI.php +++ b/src/inc/apiv2/model/CrackerBinaryAPI.php @@ -2,17 +2,24 @@ namespace Hashtopolis\inc\apiv2\model; +use Exception; use Hashtopolis\dba\AbstractModel; +use Hashtopolis\dba\ContainFilter; +use Hashtopolis\dba\Factory; use Hashtopolis\inc\utils\CrackerUtils; +use Hashtopolis\inc\utils\AccessUtils; use Hashtopolis\dba\models\CrackerBinary; +use Hashtopolis\dba\models\AccessGroup; use Hashtopolis\dba\models\CrackerBinaryType; use Hashtopolis\dba\models\Task; +use Hashtopolis\dba\models\User; use Hashtopolis\inc\apiv2\common\AbstractModelAPI; use Hashtopolis\inc\apiv2\error\HttpError; use Hashtopolis\inc\apiv2\error\HttpForbidden; use Hashtopolis\inc\apiv2\error\ResourceNotFoundError; use Hashtopolis\inc\HTException; +use Hashtopolis\inc\Util; /** @@ -27,6 +34,31 @@ public static function getDBAclass(): string { return CrackerBinary::class; } + /** + * @param CrackerBinary $object + * @throws Exception + */ + protected function getSingleACL(User $user, AbstractModel $object): bool { + return in_array( + $object->getAccessGroupId(), + Util::arrayOfIds(AccessUtils::getAccessGroupsOfUser($user)) + ); + } + + /** + * @throws Exception + */ + protected function getFilterACL(): array { + return [ + Factory::FILTER => [ + new ContainFilter( + CrackerBinary::ACCESS_GROUP_ID, + Util::arrayOfIds(AccessUtils::getAccessGroupsOfUser($this->getCurrentUser())) + ) + ] + ]; + } + /** * Extra fields which are valid for creation of object. With one of the source * fields given, the archive of the cracker binary is uploaded to the server @@ -52,6 +84,12 @@ public static function getToOneRelationships(): array { 'relationType' => CrackerBinaryType::class, 'relationKey' => CrackerBinaryType::CRACKER_BINARY_TYPE_ID, ], + 'accessGroup' => [ + 'key' => CrackerBinary::ACCESS_GROUP_ID, + + 'relationType' => AccessGroup::class, + 'relationKey' => AccessGroup::ACCESS_GROUP_ID, + ], ]; } @@ -83,7 +121,9 @@ protected function createObject(array $data): int { $data[CrackerBinary::BINARY_NAME], $data[CrackerBinary::CRACKER_BINARY_TYPE_ID], $data["sourceType"], - $data["sourceData"] + $data["sourceData"], + $data[CrackerBinary::ACCESS_GROUP_ID], + $this->getCurrentUser() ); return $binary->getId(); } @@ -94,7 +134,9 @@ protected function createObject(array $data): int { $data[CrackerBinary::VERSION], $data[CrackerBinary::BINARY_NAME], $data[CrackerBinary::DOWNLOAD_URL], - $data[CrackerBinary::CRACKER_BINARY_TYPE_ID] + $data[CrackerBinary::CRACKER_BINARY_TYPE_ID], + $data[CrackerBinary::ACCESS_GROUP_ID], + $this->getCurrentUser() ); return $binary->getId(); } @@ -107,6 +149,19 @@ protected function deleteObject(AbstractModel $object): void { CrackerUtils::deleteBinary($object->getId()); } + /** + * The access group of a binary can be changed, but only to a group the user is + * also a member of (and only from a group the user has access to). + * + * @param int $id + * @param User $current_user + */ + protected function getUpdateHandlers($id, $current_user): array { + return [ + CrackerBinary::ACCESS_GROUP_ID => fn($value) => CrackerUtils::changeAccessGroup($id, $value, $current_user) + ]; + } + /** * The download url of locally stored binaries is owned by the server, so it * cannot be overwritten with a patch. diff --git a/src/inc/apiv2/openapi/SpecOverrides.php b/src/inc/apiv2/openapi/SpecOverrides.php index a073fc873..76164e3fc 100644 --- a/src/inc/apiv2/openapi/SpecOverrides.php +++ b/src/inc/apiv2/openapi/SpecOverrides.php @@ -124,6 +124,7 @@ public static function defaults(): self { ], 'CrackerBinary' => [ self::ATTRIBUTE_DESCRIPTIONS => [ + 'accessGroupId' => 'Access group containing this cracker binary. Required on creation; the requesting user must belong to the group. It can be changed only when the user belongs to both the current and new groups.', 'downloadUrl' => 'External url where the agent downloads the binary archive from. Mutually exclusive with sourceType: when the archive is uploaded with sourceType, this url is set automatically to the download endpoint of this server and cannot be changed afterwards.', 'filename' => 'Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided.', 'sourceType' => 'Source the 7z archive is uploaded from: inline (base64 archive data in sourceData), import (filename of a file in the import directory as sourceData) or url (http/https url in sourceData, fetched by the server). Mutually exclusive with downloadUrl.', diff --git a/src/inc/downloadapi/CrackerBinaryDownloadHandler.php b/src/inc/downloadapi/CrackerBinaryDownloadHandler.php index 72fb0c03f..1b0c34d97 100644 --- a/src/inc/downloadapi/CrackerBinaryDownloadHandler.php +++ b/src/inc/downloadapi/CrackerBinaryDownloadHandler.php @@ -4,10 +4,13 @@ use Hashtopolis\dba\Factory; use Hashtopolis\dba\models\Agent; +use Hashtopolis\dba\models\User; use Hashtopolis\inc\agentapi\common\AgentAction; use Hashtopolis\inc\defines\DServerLog; use Hashtopolis\inc\utils\CrackerUtils; use Hashtopolis\inc\utils\DownloadUtils; +use Hashtopolis\inc\utils\AccessUtils; +use Hashtopolis\inc\Util; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; @@ -28,9 +31,16 @@ public function __invoke(Request $request, Response $response, array $args): Res $agent = $request->getAttribute(AgentAction::AGENT_ATTRIBUTE); if ($agent instanceof Agent) { + if (!in_array($binary->getAccessGroupId(), Util::arrayOfIds(AccessUtils::getAccessGroupsOfAgent($agent)))) { + return $response->withStatus(403); + } DServerLog::log(DServerLog::TRACE, 'Agent ' . $agent->getId() . ' downloaded the archive of cracker binary ' . $binary->getId()); } else { + $user = Factory::getUserFactory()->get((int)$request->getAttribute('userId')); + if (!$user instanceof User || !AccessUtils::userCanAccessCrackerBinary($binary, $user)) { + return $response->withStatus(403); + } DServerLog::log(DServerLog::TRACE, 'User ' . ($request->getAttribute('userId') ?? 'unknown') . ' downloaded the archive of cracker binary ' . $binary->getId()); } diff --git a/src/inc/handlers/CrackerHandler.php b/src/inc/handlers/CrackerHandler.php index 64e3dd203..c455dceec 100644 --- a/src/inc/handlers/CrackerHandler.php +++ b/src/inc/handlers/CrackerHandler.php @@ -3,6 +3,7 @@ namespace Hashtopolis\inc\handlers; use Hashtopolis\inc\utils\AccessControl; +use Hashtopolis\inc\utils\AccessUtils; use Hashtopolis\inc\utils\CrackerUtils; use Throwable; use Hashtopolis\inc\defines\DCrackerBinaryAction; @@ -32,7 +33,7 @@ public function handle($action): void { die(); case DCrackerBinaryAction::CREATE_BINARY: AccessControl::getInstance()->checkPermission(DCrackerBinaryAction::CREATE_BINARY_PERM); - $binary = CrackerUtils::createBinary($_POST['version'], $_POST['name'], $_POST['url'], $_POST['binaryTypeId']); + $binary = CrackerUtils::createBinary($_POST['version'], $_POST['name'], $_POST['url'], $_POST['binaryTypeId'], AccessUtils::getOrCreateDefaultAccessGroup()->getId(), AccessControl::getInstance()->getUser()); header("Location: crackers.php?id=" . $binary->getCrackerBinaryTypeId()); die(); case DCrackerBinaryAction::EDIT_BINARY: diff --git a/src/inc/handlers/SupertaskHandler.php b/src/inc/handlers/SupertaskHandler.php index ba69a3e95..91b8e6b83 100644 --- a/src/inc/handlers/SupertaskHandler.php +++ b/src/inc/handlers/SupertaskHandler.php @@ -27,7 +27,7 @@ public function handle($action): void { break; case DSupertaskAction::APPLY_SUPERTASK: AccessControl::getInstance()->checkPermission(DSupertaskAction::APPLY_SUPERTASK_PERM); - SupertaskUtils::runSupertask($_POST['supertask'], $_POST['hashlist'], $_POST['crackerBinaryVersionId']); + SupertaskUtils::runSupertask($_POST['supertask'], $_POST['hashlist'], $_POST['crackerBinaryVersionId'], Login::getInstance()->getUser()); header("Location: tasks.php"); die(); case DSupertaskAction::IMPORT_SUPERTASK: @@ -55,4 +55,4 @@ public function handle($action): void { UI::addMessage(UI::ERROR, $e->getMessage()); } } -} \ No newline at end of file +} diff --git a/src/inc/handlers/TaskHandler.php b/src/inc/handlers/TaskHandler.php index 14c6c86c8..871e9310d 100644 --- a/src/inc/handlers/TaskHandler.php +++ b/src/inc/handlers/TaskHandler.php @@ -4,6 +4,7 @@ use Exception; use Hashtopolis\inc\utils\AccessControl; +use Hashtopolis\inc\utils\AccessUtils; use Hashtopolis\inc\DataSet; use Throwable; use Hashtopolis\inc\utils\FileDownloadUtils; @@ -237,6 +238,10 @@ private function create() { UI::addMessage(UI::ERROR, "Non-matching cracker binary selection!"); return; } + else if (!AccessUtils::userCanAccessCrackerBinary($crackerBinary, Login::getInstance()->getUser())) { + UI::addMessage(UI::ERROR, "No access to this cracker binary!"); + return; + } else if ($chunk < 0 || $status < 0 || $chunk < $status) { UI::addMessage(UI::ERROR, "Chunk time must be higher than status timer!"); return; diff --git a/src/inc/startup/setup.json b/src/inc/startup/setup.json index 7212225a5..2f6e72922 100644 --- a/src/inc/startup/setup.json +++ b/src/inc/startup/setup.json @@ -416,7 +416,8 @@ "version" : "7.1.2", "downloadUrl" : "https://hashcat.net/files/hashcat-7.1.2.7z", "binaryName" : "hashcat", - "filename" : null + "filename" : null, + "accessGroupId" : 1 } ], "CrackerBinaryType" : [ diff --git a/src/inc/user_api/UserAPICracker.php b/src/inc/user_api/UserAPICracker.php index fedf9e936..88fdc818d 100644 --- a/src/inc/user_api/UserAPICracker.php +++ b/src/inc/user_api/UserAPICracker.php @@ -2,6 +2,7 @@ namespace Hashtopolis\inc\user_api; +use Hashtopolis\inc\utils\AccessUtils; use Hashtopolis\inc\utils\CrackerUtils; use Throwable; use Hashtopolis\inc\defines\UQuery; @@ -68,7 +69,7 @@ private function addVersion($QUERY) { throw new HTException("Invalid query!"); } $cracker = CrackerUtils::getBinaryType($QUERY[UQueryCracker::CRACKER_ID]); - CrackerUtils::createBinary($QUERY[UQueryCracker::BINARY_VERSION], $QUERY[UQueryCracker::BINARY_NAME], $QUERY[UQueryCracker::BINARY_URL], $cracker->getId()); + CrackerUtils::createBinary($QUERY[UQueryCracker::BINARY_VERSION], $QUERY[UQueryCracker::BINARY_NAME], $QUERY[UQueryCracker::BINARY_URL], $cracker->getId(), AccessUtils::getOrCreateDefaultAccessGroup()->getId(), $this->user); $this->sendSuccessResponse($QUERY); } diff --git a/src/inc/user_api/UserAPITask.php b/src/inc/user_api/UserAPITask.php index 6e77f6692..ccfeeb840 100644 --- a/src/inc/user_api/UserAPITask.php +++ b/src/inc/user_api/UserAPITask.php @@ -350,7 +350,7 @@ private function runSupertask($QUERY) { if (!isset($QUERY[UQueryTask::SUPERTASK_ID]) || !isset($QUERY[UQueryTask::TASK_HASHLIST]) || !isset($QUERY[UQueryTask::TASK_CRACKER_VERSION])) { throw new HTException("Invalid query!"); } - SupertaskUtils::runSupertask($QUERY[UQueryTask::SUPERTASK_ID], $QUERY[UQueryTask::TASK_HASHLIST], $QUERY[UQueryTask::TASK_CRACKER_VERSION]); + SupertaskUtils::runSupertask($QUERY[UQueryTask::SUPERTASK_ID], $QUERY[UQueryTask::TASK_HASHLIST], $QUERY[UQueryTask::TASK_CRACKER_VERSION], $this->user); $this->sendSuccessResponse($QUERY); } @@ -362,7 +362,7 @@ private function runPretask($QUERY) { if (!isset($QUERY[UQueryTask::PRETASK_ID]) || !isset($QUERY[UQueryTask::TASK_HASHLIST]) || !isset($QUERY[UQueryTask::TASK_CRACKER_VERSION])) { throw new HTException("Invalid query!"); } - PretaskUtils::runPretask($QUERY[UQueryTask::PRETASK_ID], $QUERY[UQueryTask::TASK_HASHLIST], $QUERY[UQueryTask::TASK_NAME], $QUERY[UQueryTask::TASK_CRACKER_VERSION]); + PretaskUtils::runPretask($QUERY[UQueryTask::PRETASK_ID], $QUERY[UQueryTask::TASK_HASHLIST], $QUERY[UQueryTask::TASK_NAME], $QUERY[UQueryTask::TASK_CRACKER_VERSION], $this->user); $this->sendSuccessResponse($QUERY); } diff --git a/src/inc/utils/AccessGroupUtils.php b/src/inc/utils/AccessGroupUtils.php index d2fd8cedb..32cbe4b3d 100644 --- a/src/inc/utils/AccessGroupUtils.php +++ b/src/inc/utils/AccessGroupUtils.php @@ -5,6 +5,7 @@ use Exception; use Hashtopolis\dba\models\AccessGroup; use Hashtopolis\dba\models\Chunk; +use Hashtopolis\dba\models\CrackerBinary; use Hashtopolis\dba\ContainFilter; use Hashtopolis\dba\models\TaskWrapper; use Hashtopolis\dba\UpdateSet; @@ -215,7 +216,12 @@ public static function deleteGroup(int $groupId): void { $qF = new QueryFilter(File::ACCESS_GROUP_ID, $group->getId(), "="); $uS = new UpdateSet(File::ACCESS_GROUP_ID, $default->getId()); Factory::getFileFactory()->massUpdate([Factory::FILTER => $qF, Factory::UPDATE => $uS]); - + + // update associations of cracker binaries with this group + $qF = new QueryFilter(CrackerBinary::ACCESS_GROUP_ID, $group->getId(), "="); + $uS = new UpdateSet(CrackerBinary::ACCESS_GROUP_ID, $default->getId()); + Factory::getCrackerBinaryFactory()->massUpdate([Factory::FILTER => $qF, Factory::UPDATE => $uS]); + // delete all associations to users $qF = new QueryFilter(AccessGroupUser::ACCESS_GROUP_ID, $group->getId(), "="); Factory::getAccessGroupUserFactory()->massDeletion([Factory::FILTER => $qF]); diff --git a/src/inc/utils/AccessUtils.php b/src/inc/utils/AccessUtils.php index 8c075b2f9..ecb625b4f 100644 --- a/src/inc/utils/AccessUtils.php +++ b/src/inc/utils/AccessUtils.php @@ -14,6 +14,7 @@ use Hashtopolis\dba\models\Hashlist; use Hashtopolis\dba\Factory; use Hashtopolis\dba\models\File; +use Hashtopolis\dba\models\CrackerBinary; use Hashtopolis\dba\models\Task; use Hashtopolis\inc\apiv2\common\AbstractBaseAPI; use Hashtopolis\inc\Util; @@ -119,6 +120,13 @@ public static function userCanAccessFile(File $file, User $user): bool { } return true; } + + /** + * @throws Exception + */ + public static function userCanAccessCrackerBinary(CrackerBinary $binary, User $user): bool { + return in_array($binary->getAccessGroupId(), Util::getAccessGroupIds($user->getId())); + } /** * @param $accessGroupsAgent AccessGroup[] @@ -197,6 +205,11 @@ public static function agentCanAccessTask(Agent $agent, Task $task): bool { if (!in_array($taskWrapper->getAccessGroupId(), $accessGroupsIds)) { return false; // task is in an access group which agent is not allowed to access } + + $crackerBinary = Factory::getCrackerBinaryFactory()->get($task->getCrackerBinaryId()); + if ($crackerBinary === null || !in_array($crackerBinary->getAccessGroupId(), $accessGroupsIds)) { + return false; + } $hashlists = Util::checkSuperHashlist(Factory::getHashlistFactory()->get($taskWrapper->getHashlistId())); foreach ($hashlists as $hashlist) { diff --git a/src/inc/utils/CrackerBinaryUtils.php b/src/inc/utils/CrackerBinaryUtils.php index 77aa2e8c0..a9e1a20b8 100644 --- a/src/inc/utils/CrackerBinaryUtils.php +++ b/src/inc/utils/CrackerBinaryUtils.php @@ -5,20 +5,31 @@ use Exception; use Hashtopolis\dba\models\CrackerBinary; use Hashtopolis\dba\QueryFilter; +use Hashtopolis\dba\ContainFilter; use Hashtopolis\dba\Factory; +use Hashtopolis\dba\models\User; use Composer\Semver\Comparator; use Hashtopolis\inc\HTException; +use Hashtopolis\inc\Util; class CrackerBinaryUtils { /** + * Returns the newest version of a cracker binary type. When a user is given, only + * binaries of access groups the user is a member of are considered. + * * @param int $crackerBinaryTypeId + * @param User|null $user * @return CrackerBinary|null * @throws HTException * @throws Exception */ - public static function getNewestVersion(int $crackerBinaryTypeId): ?CrackerBinary { - $qF = new QueryFilter(CrackerBinary::CRACKER_BINARY_TYPE_ID, $crackerBinaryTypeId, "="); - $binaries = Factory::getCrackerBinaryFactory()->filter([Factory::FILTER => $qF]); + public static function getNewestVersion(int $crackerBinaryTypeId, ?User $user = null): ?CrackerBinary { + $qFs = [new QueryFilter(CrackerBinary::CRACKER_BINARY_TYPE_ID, $crackerBinaryTypeId, "=")]; + if ($user !== null) { + // only binaries of access groups the user is a member of can be used + $qFs[] = new ContainFilter(CrackerBinary::ACCESS_GROUP_ID, Util::arrayOfIds(AccessUtils::getAccessGroupsOfUser($user))); + } + $binaries = Factory::getCrackerBinaryFactory()->filter([Factory::FILTER => $qFs]); /** @var ?CrackerBinary $newest */ $newest = null; foreach ($binaries as $binary) { diff --git a/src/inc/utils/CrackerUtils.php b/src/inc/utils/CrackerUtils.php index 59ac0a56c..4ab15507f 100644 --- a/src/inc/utils/CrackerUtils.php +++ b/src/inc/utils/CrackerUtils.php @@ -10,6 +10,7 @@ use Hashtopolis\dba\ContainFilter; use Hashtopolis\dba\Factory; use Hashtopolis\dba\models\Pretask; +use Hashtopolis\dba\models\User; use Hashtopolis\inc\defines\DDirectories; use Hashtopolis\inc\apiv2\error\HttpConflict; use Hashtopolis\inc\apiv2\error\HttpError; @@ -60,17 +61,20 @@ public static function createBinaryType(string $typeName): CrackerBinaryType { * @param string $name * @param string $url * @param int $binaryTypeId + * @param int $accessGroupId access group the binary belongs to + * @param User|null $user if given, the user must be a member of the access group * @return CrackerBinary * @throws HttpError * @throws HTException * @throws Exception */ - public static function createBinary(string $version, string $name, string $url, int $binaryTypeId): CrackerBinary { + public static function createBinary(string $version, string $name, string $url, int $binaryTypeId, int $accessGroupId, ?User $user = null): CrackerBinary { $binaryType = CrackerUtils::getBinaryType($binaryTypeId); if (strlen($version) == 0 || strlen($name) == 0 || strlen($url) == 0) { throw new HttpError("Please provide all information!"); } - $binary = new CrackerBinary(null, $binaryType->getId(), $version, $url, $name, null); + CrackerUtils::checkAccessGroup($accessGroupId, $user); + $binary = new CrackerBinary(null, $binaryType->getId(), $version, $url, $name, null, $accessGroupId); return Factory::getCrackerBinaryFactory()->save($binary); } @@ -84,16 +88,19 @@ public static function createBinary(string $version, string $name, string $url, * @param int $binaryTypeId * @param string $sourceType choices inline, import, url * @param string $sourceData base64 data, filename in the import directory or download url + * @param int $accessGroupId access group the binary belongs to + * @param User|null $user if given, the user must be a member of the access group * @return CrackerBinary * @throws HttpError * @throws HTException * @throws Exception */ - public static function createBinaryFromUpload(string $version, string $name, int $binaryTypeId, string $sourceType, string $sourceData): CrackerBinary { + public static function createBinaryFromUpload(string $version, string $name, int $binaryTypeId, string $sourceType, string $sourceData, int $accessGroupId, ?User $user = null): CrackerBinary { $binaryType = CrackerUtils::getBinaryType($binaryTypeId); if (strlen($version) == 0 || strlen($name) == 0 || strlen($sourceData) == 0) { throw new HttpError("Please provide all information!"); } + CrackerUtils::checkAccessGroup($accessGroupId, $user); // determine the source of the archive and validate it switch ($sourceType) { @@ -130,7 +137,7 @@ public static function createBinaryFromUpload(string $version, string $name, int // create the entry first with a placeholder download url, the final one // contains the id and can only be set once it is known $binary = Factory::getCrackerBinaryFactory()->save( - new CrackerBinary(null, $binaryType->getId(), $version, "", $name, null) + new CrackerBinary(null, $binaryType->getId(), $version, "", $name, null, $accessGroupId) ); $target = CrackerUtils::getCrackersPath() . $binary->getId() . '_' . $filename; @@ -286,6 +293,51 @@ public static function updateBinary(string $version, string $name, string $url, return Factory::getCrackerBinaryTypeFactory()->get($binary->getCrackerBinaryTypeId()); } + /** + * Ensures the access group exists and the user is a member of it, so binaries + * can only be created by members of the group they are created in. Callers + * without a group input (legacy UI or user api) use the default access group. + * + * @throws HttpError + * @throws Exception + */ + private static function checkAccessGroup(int $accessGroupId, ?User $user): void { + $accessGroup = Factory::getAccessGroupFactory()->get($accessGroupId); + if ($accessGroup === null) { + throw new HttpError("Invalid access group selected!"); + } + if ($user !== null && sizeof(AccessUtils::intersection( + array($accessGroup), AccessUtils::getAccessGroupsOfUser($user))) == 0) { + throw new HttpError("Access group with no rights selected!"); + } + } + + /** + * Moves a cracker binary to another access group. The user must be a member of + * the current and of the new access group. + * + * @param int $binaryId + * @param int $accessGroupId + * @param User $user + * @throws HttpError + * @throws HTException + * @throws Exception + */ + public static function changeAccessGroup(int $binaryId, int $accessGroupId, User $user): void { + $binary = CrackerUtils::getBinary($binaryId); + if (Factory::getAccessGroupFactory()->get($accessGroupId) === null) { + throw new HttpError("Invalid access group selected!"); + } + $userAccessGroupIds = Util::getAccessGroupIds($user->getId()); + if (!in_array($accessGroupId, $userAccessGroupIds) || !in_array($binary->getAccessGroupId(), $userAccessGroupIds)) { + throw new HttpError("No access to this group!"); + } + if ($binary->getAccessGroupId() == $accessGroupId) { + return; + } + Factory::getCrackerBinaryFactory()->set($binary, CrackerBinary::ACCESS_GROUP_ID, $accessGroupId); + } + /** * @param int $binaryTypeId * @return CrackerBinaryType diff --git a/src/inc/utils/HashlistUtils.php b/src/inc/utils/HashlistUtils.php index 4590911f5..fff865da5 100644 --- a/src/inc/utils/HashlistUtils.php +++ b/src/inc/utils/HashlistUtils.php @@ -131,6 +131,14 @@ public static function applyPreconfTasks(int $hashlistId, array $pretasks, User foreach ($pretasks as $pretask) { $task = Factory::getPretaskFactory()->get($pretask); if ($task != null) { + // skip pretasks of which the user has no accessible binary version, the + // newest version of the cracker type in one of the groups of the user is used + try { + $crackerBinaryId = CrackerBinaryUtils::getNewestVersion($task->getCrackerBinaryTypeId(), $user)->getId(); + } + catch (HTException $e) { + continue; + } if ($hashlist->getHexSalt() == 1 && !str_contains($task->getAttackCmd(), "--hex-salt")) { $task->setAttackCmd("--hex-salt " . $task->getAttackCmd()); } @@ -157,7 +165,7 @@ public static function applyPreconfTasks(int $hashlistId, array $pretasks, User $task->getIsCpuTask(), $task->getUseNewBench(), 0, - CrackerBinaryUtils::getNewestVersion($task->getCrackerBinaryTypeId())->getId(), + $crackerBinaryId, $task->getCrackerBinaryTypeId(), $taskWrapper->getId(), 0, diff --git a/src/inc/utils/PretaskUtils.php b/src/inc/utils/PretaskUtils.php index 16a9a2462..469a45ffa 100644 --- a/src/inc/utils/PretaskUtils.php +++ b/src/inc/utils/PretaskUtils.php @@ -8,6 +8,7 @@ use Hashtopolis\dba\models\FilePretask; use Hashtopolis\dba\models\TaskWrapper; use Hashtopolis\dba\models\Task; +use Hashtopolis\dba\models\User; use Hashtopolis\dba\OrderFilter; use Hashtopolis\dba\QueryFilter; use Hashtopolis\dba\models\SupertaskPretask; @@ -237,7 +238,7 @@ public static function getPretask(int $pretaskId): Pretask { * @throws HTException * @throws Exception */ - public static function runPretask(int $pretaskId, int $hashlistId, string $name, int $crackerBinaryId): void { + public static function runPretask(int $pretaskId, int $hashlistId, string $name, int $crackerBinaryId, User $user): void { $pretask = Factory::getPretaskFactory()->get($pretaskId); if ($pretask == null) { throw new HTException("Invalid preconfigured task ID!"); @@ -257,6 +258,9 @@ public static function runPretask(int $pretaskId, int $hashlistId, string $name, else if ($pretask->getCrackerBinaryTypeId() != $cracker->getCrackerBinaryTypeId()) { throw new HTException("Provided cracker does not match the type of the pretask!"); } + else if (!AccessUtils::userCanAccessCrackerBinary($cracker, $user)) { + throw new HTException("You have no access to this cracker binary!"); + } Factory::getAgentFactory()->getDB()->beginTransaction(); $taskWrapper = new TaskWrapper(null, $pretask->getPriority(), $pretask->getMaxAgents(), DTaskTypes::NORMAL, $hashlist->getId(), $hashlist->getAccessGroupId(), "", 0, 0); @@ -378,4 +382,3 @@ public static function createPretask(string $name, string $cmdLine, int $chunkTi return $pretask; } } - diff --git a/src/inc/utils/SupertaskUtils.php b/src/inc/utils/SupertaskUtils.php index c31b2b1ec..b944954df 100644 --- a/src/inc/utils/SupertaskUtils.php +++ b/src/inc/utils/SupertaskUtils.php @@ -259,7 +259,7 @@ public static function getSupertask(int $supertaskId): Supertask { * @throws HTException * @throws Exception */ - public static function runSupertask(int $supertaskId, int $hashlistId, int $crackerId): void { + public static function runSupertask(int $supertaskId, int $hashlistId, int $crackerId, User $user): void { $supertask = Factory::getSupertaskFactory()->get($supertaskId); if ($supertask == null) { throw new HTException("Invalid supertask ID!"); @@ -275,6 +275,9 @@ public static function runSupertask(int $supertaskId, int $hashlistId, int $crac if ($cracker == null) { throw new HTException("Invalid cracker ID!"); } + else if (!AccessUtils::userCanAccessCrackerBinary($cracker, $user)) { + throw new HTException("You have no access to this cracker binary!"); + } $qF = new QueryFilter(SupertaskPretask::SUPERTASK_ID, $supertask->getId(), "=", Factory::getSupertaskPretaskFactory()); $jF = new JoinFilter(Factory::getSupertaskPretaskFactory(), Pretask::PRETASK_ID, SupertaskPretask::PRETASK_ID); $joined = Factory::getPretaskFactory()->filter([Factory::FILTER => $qF, Factory::JOIN => $jF]); @@ -297,7 +300,7 @@ public static function runSupertask(int $supertaskId, int $hashlistId, int $crac foreach ($pretasks as $pretask) { $crackerBinaryId = $cracker->getId(); if ($cracker->getCrackerBinaryTypeId() != $pretask->getCrackerBinaryTypeId()) { - $crackerBinaryId = CrackerBinaryUtils::getNewestVersion($pretask->getCrackerBinaryTypeId())->getId(); + $crackerBinaryId = CrackerBinaryUtils::getNewestVersion($pretask->getCrackerBinaryTypeId(), $user)->getId(); } $task = new Task( diff --git a/src/inc/utils/TaskUtils.php b/src/inc/utils/TaskUtils.php index 2afa8ba28..420045a57 100644 --- a/src/inc/utils/TaskUtils.php +++ b/src/inc/utils/TaskUtils.php @@ -897,6 +897,9 @@ public static function createTask(int $hashlistId, string $name, string $attackC if ($cracker == null) { throw new HttpError("Invalid cracker ID!"); } + else if (!AccessUtils::userCanAccessCrackerBinary($cracker, $user)) { + throw new HttpForbidden("You have no access to this cracker binary!"); + } else if (!str_contains($attackCmd, SConfig::getInstance()->getVal(DConfig::HASHLIST_ALIAS))) { throw new HttpError("Attack command does not contain hashlist alias!"); } @@ -1103,6 +1106,10 @@ private static function getCandidateTasks(Agent $agent, array $accessGroups, Tas $permitted = false; } } + $crackerBinary = Factory::getCrackerBinaryFactory()->get($task->getCrackerBinaryId()); + if ($crackerBinary === null || !in_array($crackerBinary->getAccessGroupId(), $accessGroups)) { + continue; + } if (!$permitted) { continue; // at least one of the files required for this task is secret and the agent not, so this task cannot be used } diff --git a/src/migrations/mysql/20260903211245_cracker-access-groups.sql b/src/migrations/mysql/20260903211245_cracker-access-groups.sql new file mode 100644 index 000000000..96cdb5659 --- /dev/null +++ b/src/migrations/mysql/20260903211245_cracker-access-groups.sql @@ -0,0 +1,8 @@ +-- Cracker binaries belong to an access group. Existing rows are assigned to the +-- default access group. +ALTER TABLE CrackerBinary ADD COLUMN accessGroupId int NOT NULL DEFAULT 1 AFTER filename; +ALTER TABLE CrackerBinary ADD KEY `accessGroupId` (`accessGroupId`); +ALTER TABLE CrackerBinary ADD CONSTRAINT `CrackerBinary_ibfk_2` FOREIGN KEY (`accessGroupId`) REFERENCES `AccessGroup` (`accessGroupId`); + +-- the access group must be provided explicitly, no silent default for new rows +ALTER TABLE CrackerBinary ALTER accessGroupId DROP DEFAULT; diff --git a/src/migrations/postgres/20260903211245_cracker-access-groups.sql b/src/migrations/postgres/20260903211245_cracker-access-groups.sql new file mode 100644 index 000000000..0cdc649f5 --- /dev/null +++ b/src/migrations/postgres/20260903211245_cracker-access-groups.sql @@ -0,0 +1,8 @@ +-- Cracker binaries belong to an access group. Existing rows are assigned to the +-- default access group. +ALTER TABLE CrackerBinary ADD COLUMN accessGroupId INT NOT NULL DEFAULT 1; +CREATE INDEX IF NOT EXISTS crackerbinary_accessgroupid_idx ON CrackerBinary(accessGroupId); +ALTER TABLE ONLY CrackerBinary ADD CONSTRAINT crackerbinary_ibfk_2 FOREIGN KEY (accessGroupId) REFERENCES AccessGroup(accessGroupId); + +-- the access group must be provided explicitly, no silent default for new rows +ALTER TABLE CrackerBinary ALTER COLUMN accessGroupId DROP DEFAULT;