From fadc0cea7f3bc60e43984449183fbd0d376aa38d Mon Sep 17 00:00:00 2001 From: Sein Coray Date: Thu, 3 Sep 2026 21:14:00 +0200 Subject: [PATCH 1/5] first step in adding access groups to cracker binary and type --- ci/apiv2/test_taskwrapper.py | 3 +- .../testfiles/cracker/create_cracker_001.json | 4 +- .../testfiles/cracker/create_cracker_002.json | 4 +- .../crackertype/create_crackertype_001.json | 3 +- .../crackertype/create_crackertype_002.json | 3 +- ci/phpunit/TestBase.php | 4 +- ci/phpunit/dba/AbstractModelFactoryTest.php | 4 +- ci/phpunit/dba/MassUpdateSetTest.php | 4 +- ci/phpunit/downloadapi/DownloadAppTest.php | 4 +- .../openapi/crackerbinarytype.spec.json | 54 +++++-- ci/phpunit/inc/UtilTest.php | 7 +- .../inc/utils/CrackerBinaryUtilsTest.php | 4 +- ci/phpunit/inc/utils/CrackerUtilsTest.php | 8 +- openapi.json | 150 +++++++++++++++--- src/dba/models/CrackerBinary.php | 15 +- src/dba/models/CrackerBinaryFactory.php | 4 +- src/dba/models/CrackerBinaryType.php | 15 +- src/dba/models/CrackerBinaryTypeFactory.php | 4 +- src/dba/models/generator.php | 6 + src/inc/apiv2/model/CrackerBinaryAPI.php | 6 +- src/inc/apiv2/model/CrackerBinaryTypeAPI.php | 2 +- src/inc/startup/setup.json | 6 +- src/inc/utils/CrackerUtils.php | 37 ++++- .../20260903211245_cracker-access-groups.sql | 13 ++ .../20260903211245_cracker-access-groups.sql | 14 ++ 25 files changed, 303 insertions(+), 75 deletions(-) create mode 100644 src/migrations/mysql/20260903211245_cracker-access-groups.sql create mode 100644 src/migrations/postgres/20260903211245_cracker-access-groups.sql diff --git a/ci/apiv2/test_taskwrapper.py b/ci/apiv2/test_taskwrapper.py index 5615c391c..4cb214b6b 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=crackertype.accessGroupId) 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/apiv2/testfiles/crackertype/create_crackertype_001.json b/ci/apiv2/testfiles/crackertype/create_crackertype_001.json index 9082b7f34..7314e24ca 100644 --- a/ci/apiv2/testfiles/crackertype/create_crackertype_001.json +++ b/ci/apiv2/testfiles/crackertype/create_crackertype_001.json @@ -1,4 +1,5 @@ { "typeName": "generic2", - "isChunkingAvailable": true + "isChunkingAvailable": true, + "accessGroupId": 1 } diff --git a/ci/apiv2/testfiles/crackertype/create_crackertype_002.json b/ci/apiv2/testfiles/crackertype/create_crackertype_002.json index d4efcd601..0ec27d8fa 100644 --- a/ci/apiv2/testfiles/crackertype/create_crackertype_002.json +++ b/ci/apiv2/testfiles/crackertype/create_crackertype_002.json @@ -1,4 +1,5 @@ { "typeName": true, - "isChunkingAvailable": true + "isChunkingAvailable": true, + "accessGroupId": 1 } diff --git a/ci/phpunit/TestBase.php b/ci/phpunit/TestBase.php index 4340a5f5a..cf9d1ee0f 100644 --- a/ci/phpunit/TestBase.php +++ b/ci/phpunit/TestBase.php @@ -201,7 +201,7 @@ protected function createTaskWrapper(AccessGroup $group, Hashlist $hashlist, int protected function createCrackerBinaryType(): CrackerBinaryType { $crackerBinaryType = $this->createDatabaseObject( Factory::getCrackerBinaryTypeFactory(), - new CrackerBinaryType(null, 'type_' . uniqid(), 1) + new CrackerBinaryType(null, 'type_' . uniqid(), 1, 1) ); $this->assertTrue($crackerBinaryType instanceof CrackerBinaryType); return $crackerBinaryType; @@ -213,7 +213,7 @@ protected function createCrackerBinaryType(): CrackerBinaryType { protected function createCrackerBinary(CrackerBinaryType $crackerBinaryType): 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, 1) ); $this->assertTrue($crackerBinary instanceof CrackerBinary); return $crackerBinary; diff --git a/ci/phpunit/dba/AbstractModelFactoryTest.php b/ci/phpunit/dba/AbstractModelFactoryTest.php index 18adb7feb..91b8ad037 100644 --- a/ci/phpunit/dba/AbstractModelFactoryTest.php +++ b/ci/phpunit/dba/AbstractModelFactoryTest.php @@ -1678,10 +1678,10 @@ private function setUpHealthCheck(): array { $hashType = new HashType(null, 'placeholder', 0, 0); $hashType = $this->createDatabaseObject(Factory::getHashTypeFactory(), $hashType); - $crackerBinaryType = new CrackerBinaryType(null, '', 0); + $crackerBinaryType = new CrackerBinaryType(null, '', 0, 1); $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..ed9016c15 100644 --- a/ci/phpunit/dba/MassUpdateSetTest.php +++ b/ci/phpunit/dba/MassUpdateSetTest.php @@ -158,8 +158,8 @@ public function testMassSingleUpdateWithMappedColumn(): void { $prefix = 'hca_' . $testId; $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)); + $cbt = $this->createDatabaseObject(Factory::getCrackerBinaryTypeFactory(), new CrackerBinaryType(null, '', 0, 1)); + $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..54e59d9ce 100644 --- a/ci/phpunit/downloadapi/DownloadAppTest.php +++ b/ci/phpunit/downloadapi/DownloadAppTest.php @@ -39,11 +39,11 @@ protected function setUp(): void { $this->type = $this->createDatabaseObject( Factory::getCrackerBinaryTypeFactory(), - new CrackerBinaryType(null, 'download-test-type', 1) + new CrackerBinaryType(null, 'download-test-type', 1, 1) ); $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 diff --git a/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json b/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json index 7fae5a60e..d39b7c744 100644 --- a/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json +++ b/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json @@ -1366,10 +1366,14 @@ "properties": { "typeName": { "type": "string" + }, + "accessGroupId": { + "type": "integer" } }, "required": [ - "typeName" + "typeName", + "accessGroupId" ] } } @@ -1545,7 +1549,8 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable" + "isChunkingAvailable", + "accessGroupId" ], "properties": { "typeName": { @@ -1556,6 +1561,9 @@ "boolean", "null" ] + }, + "accessGroupId": { + "type": "integer" } } }, @@ -1699,7 +1707,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -1724,6 +1733,9 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer" } } } @@ -1927,7 +1939,8 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable" + "isChunkingAvailable", + "accessGroupId" ], "properties": { "typeName": { @@ -1938,6 +1951,9 @@ "boolean", "null" ] + }, + "accessGroupId": { + "type": "integer" } } }, @@ -2081,7 +2097,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -2106,6 +2123,9 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer" } } } @@ -2309,7 +2329,8 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable" + "isChunkingAvailable", + "accessGroupId" ], "properties": { "typeName": { @@ -2320,6 +2341,9 @@ "boolean", "null" ] + }, + "accessGroupId": { + "type": "integer" } } }, @@ -2463,7 +2487,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -2488,6 +2513,9 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer" } } } @@ -2742,7 +2770,8 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable" + "isChunkingAvailable", + "accessGroupId" ], "properties": { "typeName": { @@ -2753,6 +2782,9 @@ "boolean", "null" ] + }, + "accessGroupId": { + "type": "integer" } } }, @@ -2897,7 +2929,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -2922,6 +2955,9 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer" } } } diff --git a/ci/phpunit/inc/UtilTest.php b/ci/phpunit/inc/UtilTest.php index 97dd3d118..9332c1f06 100644 --- a/ci/phpunit/inc/UtilTest.php +++ b/ci/phpunit/inc/UtilTest.php @@ -949,7 +949,7 @@ public function testCheckOrCreateInitialObjectAgentBinary(): void { */ public function testCheckOrCreateInitialObjectCrackerBinaryType(): void { $id = 9007; - $data = ['crackerBinaryTypeId' => $id, 'typeName' => 'testHashcat', 'isChunkingAvailable' => 1]; + $data = ['crackerBinaryTypeId' => $id, 'typeName' => 'testHashcat', 'isChunkingAvailable' => 1, 'accessGroupId' => 1]; Util::checkOrCreateInitialObject(Factory::getCrackerBinaryTypeFactory(), $data); $obj = Factory::getCrackerBinaryTypeFactory()->get($id); $this->assertNotNull($obj); @@ -965,7 +965,7 @@ public function testCheckOrCreateInitialObjectCrackerBinaryType(): void { */ public function testCheckOrCreateInitialObjectCrackerBinary(): void { $typeId = 9008; - $typeData = ['crackerBinaryTypeId' => $typeId, 'typeName' => 'binType', 'isChunkingAvailable' => 0]; + $typeData = ['crackerBinaryTypeId' => $typeId, 'typeName' => 'binType', 'isChunkingAvailable' => 0, 'accessGroupId' => 1]; Util::checkOrCreateInitialObject(Factory::getCrackerBinaryTypeFactory(), $typeData); $id = 9009; @@ -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/CrackerBinaryUtilsTest.php b/ci/phpunit/inc/utils/CrackerBinaryUtilsTest.php index d835092bf..7df4ba435 100644 --- a/ci/phpunit/inc/utils/CrackerBinaryUtilsTest.php +++ b/ci/phpunit/inc/utils/CrackerBinaryUtilsTest.php @@ -27,7 +27,7 @@ protected function setUp(): void { parent::setUp(); $this->type = $this->createDatabaseObject( Factory::getCrackerBinaryTypeFactory(), - new CrackerBinaryType(null, 'test-crackerbinaryutils-type', 1) + new CrackerBinaryType(null, 'test-crackerbinaryutils-type', 1, 1) ); } @@ -36,7 +36,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) ); } diff --git a/ci/phpunit/inc/utils/CrackerUtilsTest.php b/ci/phpunit/inc/utils/CrackerUtilsTest.php index 42d2272c9..2279e27d3 100644 --- a/ci/phpunit/inc/utils/CrackerUtilsTest.php +++ b/ci/phpunit/inc/utils/CrackerUtilsTest.php @@ -35,11 +35,11 @@ protected function setUp(): void { parent::setUp(); $this->type = $this->createDatabaseObject( Factory::getCrackerBinaryTypeFactory(), - new CrackerBinaryType(null, 'test-crackerutils-type', 1) + new CrackerBinaryType(null, 'test-crackerutils-type', 1, 1) ); $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) ); } @@ -155,7 +155,7 @@ public function testCreateBinaryFromUploadInlineSource(): void { public function testCreateBinaryFromUploadSanitizesFilename(): void { $type = $this->createDatabaseObject( Factory::getCrackerBinaryTypeFactory(), - new CrackerBinaryType(null, 'weird cracker name!', 1) + new CrackerBinaryType(null, 'weird cracker name!', 1, 1) ); $b = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $type->getId(), 'inline', base64_encode(self::SEVEN_ZIP_MAGIC)); $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $b); @@ -252,7 +252,7 @@ public function testDeleteBinaryRemovesLocalArchive(): void { public function testDeleteBinaryTypeRemovesLocalArchives(): void { $type = $this->createDatabaseObject( Factory::getCrackerBinaryTypeFactory(), - new CrackerBinaryType(null, 'type2-' . uniqid(), 1) + new CrackerBinaryType(null, 'type2-' . uniqid(), 1, 1) ); $name = 'test-archive-' . uniqid() . '.7z'; file_put_contents($this->getImportPath() . $name, self::SEVEN_ZIP_MAGIC . 'to-be-deleted'); diff --git a/openapi.json b/openapi.json index bce35d5d3..cc21508a7 100644 --- a/openapi.json +++ b/openapi.json @@ -41904,12 +41904,16 @@ }, "binaryName": { "type": "string" + }, + "accessGroupId": { + "type": "integer" } }, "required": [ "crackerBinaryTypeId", "version", - "binaryName" + "binaryName", + "accessGroupId" ] } } @@ -42094,7 +42098,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -42119,6 +42124,9 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer" } } }, @@ -42263,7 +42271,8 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable" + "isChunkingAvailable", + "accessGroupId" ], "properties": { "typeName": { @@ -42274,6 +42283,9 @@ "boolean", "null" ] + }, + "accessGroupId": { + "type": "integer" } } } @@ -42480,7 +42492,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -42505,6 +42518,9 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer" } } }, @@ -42649,7 +42665,8 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable" + "isChunkingAvailable", + "accessGroupId" ], "properties": { "typeName": { @@ -42660,6 +42677,9 @@ "boolean", "null" ] + }, + "accessGroupId": { + "type": "integer" } } } @@ -42917,7 +42937,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -42942,6 +42963,9 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer" } } }, @@ -43087,7 +43111,8 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable" + "isChunkingAvailable", + "accessGroupId" ], "properties": { "typeName": { @@ -43098,6 +43123,9 @@ "boolean", "null" ] + }, + "accessGroupId": { + "type": "integer" } } } @@ -43371,10 +43399,14 @@ "properties": { "typeName": { "type": "string" + }, + "accessGroupId": { + "type": "integer" } }, "required": [ - "typeName" + "typeName", + "accessGroupId" ] } } @@ -43550,7 +43582,8 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable" + "isChunkingAvailable", + "accessGroupId" ], "properties": { "typeName": { @@ -43561,6 +43594,9 @@ "boolean", "null" ] + }, + "accessGroupId": { + "type": "integer" } } }, @@ -43704,7 +43740,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -43729,6 +43766,9 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer" } } } @@ -43932,7 +43972,8 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable" + "isChunkingAvailable", + "accessGroupId" ], "properties": { "typeName": { @@ -43943,6 +43984,9 @@ "boolean", "null" ] + }, + "accessGroupId": { + "type": "integer" } } }, @@ -44086,7 +44130,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -44111,6 +44156,9 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer" } } } @@ -44365,7 +44413,8 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable" + "isChunkingAvailable", + "accessGroupId" ], "properties": { "typeName": { @@ -44376,6 +44425,9 @@ "boolean", "null" ] + }, + "accessGroupId": { + "type": "integer" } } }, @@ -44520,7 +44572,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -44545,6 +44598,9 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer" } } } @@ -54375,7 +54431,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -54400,6 +54457,9 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer" } } } @@ -54841,7 +54901,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -54866,6 +54927,9 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer" } } } @@ -55359,7 +55423,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -55384,6 +55449,9 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer" } } } @@ -62425,7 +62493,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -62450,6 +62519,9 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer" } } } @@ -62475,7 +62547,8 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable" + "isChunkingAvailable", + "accessGroupId" ], "properties": { "typeName": { @@ -62486,6 +62559,9 @@ "boolean", "null" ] + }, + "accessGroupId": { + "type": "integer" } } } @@ -63400,7 +63476,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -63425,6 +63502,9 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer" } } } @@ -63450,7 +63530,8 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable" + "isChunkingAvailable", + "accessGroupId" ], "properties": { "typeName": { @@ -63461,6 +63542,9 @@ "boolean", "null" ] + }, + "accessGroupId": { + "type": "integer" } } } @@ -64375,7 +64459,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -64400,6 +64485,9 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer" } } } @@ -64425,7 +64513,8 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable" + "isChunkingAvailable", + "accessGroupId" ], "properties": { "typeName": { @@ -64436,6 +64525,9 @@ "boolean", "null" ] + }, + "accessGroupId": { + "type": "integer" } } } @@ -65402,7 +65494,8 @@ "version", "downloadUrl", "binaryName", - "filename" + "filename", + "accessGroupId" ], "properties": { "crackerBinaryTypeId": { @@ -65427,6 +65520,9 @@ "null" ], "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." + }, + "accessGroupId": { + "type": "integer" } } } @@ -65452,7 +65548,8 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable" + "isChunkingAvailable", + "accessGroupId" ], "properties": { "typeName": { @@ -65463,6 +65560,9 @@ "boolean", "null" ] + }, + "accessGroupId": { + "type": "integer" } } } diff --git a/src/dba/models/CrackerBinary.php b/src/dba/models/CrackerBinary.php index 55c01cfb5..4ffa82139 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' => True, "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/CrackerBinaryType.php b/src/dba/models/CrackerBinaryType.php index d21c3f071..60cfa0b76 100644 --- a/src/dba/models/CrackerBinaryType.php +++ b/src/dba/models/CrackerBinaryType.php @@ -8,11 +8,13 @@ class CrackerBinaryType extends AbstractModel { private ?int $crackerBinaryTypeId; private ?string $typeName; private ?int $isChunkingAvailable; + private ?int $accessGroupId; - function __construct(?int $crackerBinaryTypeId, ?string $typeName, ?int $isChunkingAvailable) { + function __construct(?int $crackerBinaryTypeId, ?string $typeName, ?int $isChunkingAvailable, ?int $accessGroupId) { $this->crackerBinaryTypeId = $crackerBinaryTypeId; $this->typeName = $typeName; $this->isChunkingAvailable = $isChunkingAvailable; + $this->accessGroupId = $accessGroupId; } function getKeyValueDict(): array { @@ -20,6 +22,7 @@ function getKeyValueDict(): array { $dict['crackerBinaryTypeId'] = $this->crackerBinaryTypeId; $dict['typeName'] = $this->typeName; $dict['isChunkingAvailable'] = $this->isChunkingAvailable; + $dict['accessGroupId'] = $this->accessGroupId; return $dict; } @@ -29,6 +32,7 @@ static function getFeatures(): array { $dict['crackerBinaryTypeId'] = ['read_only' => True, "type" => "int", "subtype" => "unset", "choices" => "unset", "null" => False, "pk" => True, "protected" => True, "private" => False, "alias" => "crackerBinaryTypeId", "public" => False, "dba_mapping" => False]; $dict['typeName'] = ['read_only' => False, "type" => "str(30)", "subtype" => "unset", "choices" => "unset", "null" => False, "pk" => False, "protected" => False, "private" => False, "alias" => "typeName", "public" => False, "dba_mapping" => False]; $dict['isChunkingAvailable'] = ['read_only' => False, "type" => "bool", "subtype" => "unset", "choices" => "unset", "null" => True, "pk" => False, "protected" => False, "private" => False, "alias" => "isChunkingAvailable", "public" => False, "dba_mapping" => False]; + $dict['accessGroupId'] = ['read_only' => True, "type" => "int", "subtype" => "unset", "choices" => "unset", "null" => False, "pk" => False, "protected" => False, "private" => False, "alias" => "accessGroupId", "public" => False, "dba_mapping" => False]; return $dict; } @@ -73,9 +77,18 @@ function setIsChunkingAvailable(?int $isChunkingAvailable): void { $this->isChunkingAvailable = $isChunkingAvailable; } + function getAccessGroupId(): ?int { + return $this->accessGroupId; + } + + function setAccessGroupId(?int $accessGroupId): void { + $this->accessGroupId = $accessGroupId; + } + const CRACKER_BINARY_TYPE_ID = "crackerBinaryTypeId"; const TYPE_NAME = "typeName"; const IS_CHUNKING_AVAILABLE = "isChunkingAvailable"; + const ACCESS_GROUP_ID = "accessGroupId"; const PERM_CREATE = "permCrackerBinaryTypeCreate"; const PERM_READ = "permCrackerBinaryTypeRead"; diff --git a/src/dba/models/CrackerBinaryTypeFactory.php b/src/dba/models/CrackerBinaryTypeFactory.php index de20c9ebe..362f733fe 100644 --- a/src/dba/models/CrackerBinaryTypeFactory.php +++ b/src/dba/models/CrackerBinaryTypeFactory.php @@ -32,7 +32,7 @@ function getCacheValidTime(): int { * @return CrackerBinaryType */ function getNullObject(): CrackerBinaryType { - return new CrackerBinaryType(-1, null, null); + return new CrackerBinaryType(-1, null, null, null); } /** @@ -45,6 +45,6 @@ function createObjectFromDict(array $dict): CrackerBinaryType { $conv[strtolower($key)] = $val; } $dict = $conv; - return new CrackerBinaryType($dict['crackerbinarytypeid'], $dict['typename'], $dict['ischunkingavailable']); + return new CrackerBinaryType($dict['crackerbinarytypeid'], $dict['typename'], $dict['ischunkingavailable'], $dict['accessgroupid']); } } diff --git a/src/dba/models/generator.php b/src/dba/models/generator.php index 0220810b7..47e77484f 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 of the binary, must match the access group of its cracker binary type + // (read only until the type group move including all its binaries is implemented) + ['name' => 'accessGroupId', 'read_only' => True, 'type' => 'int', 'relation' => 'AccessGroup'], ], ]; $CONF['CrackerBinaryType'] = [ @@ -295,6 +298,9 @@ ['name' => 'crackerBinaryTypeId', 'read_only' => True, 'type' => 'int', 'protected' => True], ['name' => 'typeName', 'read_only' => False, 'type' => 'str(30)'], ['name' => 'isChunkingAvailable', 'read_only' => False, 'null' => True, 'type' => 'bool'], + // access group of the type, all its binaries are always in the same group + // (read only until the group move including all binaries is implemented) + ['name' => 'accessGroupId', 'read_only' => True, 'type' => 'int', 'relation' => 'AccessGroup'], ], ]; $CONF['File'] = [ diff --git a/src/inc/apiv2/model/CrackerBinaryAPI.php b/src/inc/apiv2/model/CrackerBinaryAPI.php index a804b80dd..0e5d5f51e 100644 --- a/src/inc/apiv2/model/CrackerBinaryAPI.php +++ b/src/inc/apiv2/model/CrackerBinaryAPI.php @@ -83,7 +83,8 @@ 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] ?? null ); return $binary->getId(); } @@ -94,7 +95,8 @@ 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] ?? null ); return $binary->getId(); } diff --git a/src/inc/apiv2/model/CrackerBinaryTypeAPI.php b/src/inc/apiv2/model/CrackerBinaryTypeAPI.php index 5beb68e87..3c1bdcbe2 100644 --- a/src/inc/apiv2/model/CrackerBinaryTypeAPI.php +++ b/src/inc/apiv2/model/CrackerBinaryTypeAPI.php @@ -60,7 +60,7 @@ function getAllPostParameters(array $features): array { * @throws HttpError */ protected function createObject(array $data): int { - $binaryType = CrackerUtils::createBinaryType($data[CrackerBinaryType::TYPE_NAME]); + $binaryType = CrackerUtils::createBinaryType($data[CrackerBinaryType::TYPE_NAME], $data[CrackerBinaryType::ACCESS_GROUP_ID] ?? null); return $binaryType->getId(); } diff --git a/src/inc/startup/setup.json b/src/inc/startup/setup.json index 7212225a5..257b185af 100644 --- a/src/inc/startup/setup.json +++ b/src/inc/startup/setup.json @@ -416,14 +416,16 @@ "version" : "7.1.2", "downloadUrl" : "https://hashcat.net/files/hashcat-7.1.2.7z", "binaryName" : "hashcat", - "filename" : null + "filename" : null, + "accessGroupId" : 1 } ], "CrackerBinaryType" : [ { "crackerBinaryTypeId" : 1, "typeName" : "hashcat", - "isChunkingAvailable" : 1 + "isChunkingAvailable" : 1, + "accessGroupId" : 1 } ], "Preprocessor" : [ diff --git a/src/inc/utils/CrackerUtils.php b/src/inc/utils/CrackerUtils.php index 59ac0a56c..ccbce2df3 100644 --- a/src/inc/utils/CrackerUtils.php +++ b/src/inc/utils/CrackerUtils.php @@ -37,12 +37,13 @@ public static function getBinaryTypes(): array { /** * @param string $typeName + * @param int|null $accessGroupId falls back to the default access group if not given * @return CrackerBinaryType * @throws HttpConflict * @throws HttpError * @throws Exception */ - public static function createBinaryType(string $typeName): CrackerBinaryType { + public static function createBinaryType(string $typeName, ?int $accessGroupId = null): CrackerBinaryType { $qF = new QueryFilter(CrackerBinaryType::TYPE_NAME, $typeName, "="); $check = Factory::getCrackerBinaryTypeFactory()->filter([Factory::FILTER => $qF], true); if ($check !== null) { @@ -51,7 +52,13 @@ public static function createBinaryType(string $typeName): CrackerBinaryType { else if (strlen($typeName) == 0) { throw new HttpError("Cracker name cannot be empty!"); } - $binaryType = new CrackerBinaryType(null, $typeName, 1); + if ($accessGroupId === null) { + $accessGroupId = AccessUtils::getOrCreateDefaultAccessGroup()->getId(); + } + else if (Factory::getAccessGroupFactory()->get($accessGroupId) === null) { + throw new HttpError("Invalid access group selected!"); + } + $binaryType = new CrackerBinaryType(null, $typeName, 1, $accessGroupId); return Factory::getCrackerBinaryTypeFactory()->save($binaryType); } @@ -60,17 +67,20 @@ public static function createBinaryType(string $typeName): CrackerBinaryType { * @param string $name * @param string $url * @param int $binaryTypeId + * @param int|null $accessGroupId must match the access group of the cracker binary type, + * falls back to the group of the type if not given * @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 = 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::checkAccessGroupMatchesType($binaryType, $accessGroupId); + $binary = new CrackerBinary(null, $binaryType->getId(), $version, $url, $name, null, $binaryType->getAccessGroupId()); return Factory::getCrackerBinaryFactory()->save($binary); } @@ -84,16 +94,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|null $accessGroupId must match the access group of the cracker binary type, + * falls back to the group of the type if not given * @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 = null): CrackerBinary { $binaryType = CrackerUtils::getBinaryType($binaryTypeId); if (strlen($version) == 0 || strlen($name) == 0 || strlen($sourceData) == 0) { throw new HttpError("Please provide all information!"); } + CrackerUtils::checkAccessGroupMatchesType($binaryType, $accessGroupId); // determine the source of the archive and validate it switch ($sourceType) { @@ -130,7 +143,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, $binaryType->getAccessGroupId()) ); $target = CrackerUtils::getCrackersPath() . $binary->getId() . '_' . $filename; @@ -286,6 +299,18 @@ public static function updateBinary(string $version, string $name, string $url, return Factory::getCrackerBinaryTypeFactory()->get($binary->getCrackerBinaryTypeId()); } + /** + * Ensures a cracker binary is created in the access group of its type, all binaries + * of a type are always in the same group. + * + * @throws HttpError + */ + private static function checkAccessGroupMatchesType(CrackerBinaryType $binaryType, ?int $accessGroupId): void { + if ($accessGroupId !== null && $accessGroupId != $binaryType->getAccessGroupId()) { + throw new HttpError("The access group must match the access group of the cracker binary type!"); + } + } + /** * @param int $binaryTypeId * @return CrackerBinaryType 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..9d5feec7d --- /dev/null +++ b/src/migrations/mysql/20260903211245_cracker-access-groups.sql @@ -0,0 +1,13 @@ +-- Cracker binaries and types belong to an access group: all binaries of a type are in +-- the same group as the type. 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`); + +ALTER TABLE CrackerBinaryType ADD COLUMN accessGroupId int NOT NULL DEFAULT 1 AFTER isChunkingAvailable; +ALTER TABLE CrackerBinaryType ADD KEY `accessGroupId` (`accessGroupId`); +ALTER TABLE CrackerBinaryType ADD CONSTRAINT `CrackerBinaryType_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; +ALTER TABLE CrackerBinaryType 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..4e0e139b1 --- /dev/null +++ b/src/migrations/postgres/20260903211245_cracker-access-groups.sql @@ -0,0 +1,14 @@ +-- Cracker binaries and types belong to an access group: all binaries of a type are in +-- the same group as the type. Existing rows are assigned to the default access group. +ALTER TABLE CrackerBinary ADD COLUMN accessGroupId INT NOT NULL DEFAULT 1; +ALTER TABLE CrackerBinaryType ADD COLUMN accessGroupId INT NOT NULL DEFAULT 1; + +CREATE INDEX IF NOT EXISTS crackerbinary_accessgroupid_idx ON CrackerBinary(accessGroupId); +CREATE INDEX IF NOT EXISTS crackerbinarytype_accessgroupid_idx ON CrackerBinaryType(accessGroupId); + +ALTER TABLE ONLY CrackerBinary ADD CONSTRAINT crackerbinary_ibfk_2 FOREIGN KEY (accessGroupId) REFERENCES AccessGroup(accessGroupId); +ALTER TABLE ONLY CrackerBinaryType ADD CONSTRAINT crackerbinarytype_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; +ALTER TABLE CrackerBinaryType ALTER COLUMN accessGroupId DROP DEFAULT; From 8ad8a11157408dfa689b80c162e0b5c869c2dd25 Mon Sep 17 00:00:00 2001 From: Sein Coray Date: Thu, 3 Sep 2026 21:17:46 +0200 Subject: [PATCH 2/5] made test more failure proof when log entries are getting deleted --- ci/apiv2/test_logentry.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 From 9041c6f204553b67625caaa523829a06f7c70cca Mon Sep 17 00:00:00 2001 From: Sein Coray Date: Thu, 3 Sep 2026 21:46:03 +0200 Subject: [PATCH 3/5] applying handling of the access group id around cracker binary, removed access group from type as it makes more sense to keep that globally --- ci/apiv2/test_taskwrapper.py | 2 +- .../crackertype/create_crackertype_001.json | 3 +- .../crackertype/create_crackertype_002.json | 3 +- ci/phpunit/TestBase.php | 2 +- ci/phpunit/dba/AbstractModelFactoryTest.php | 2 +- ci/phpunit/dba/MassUpdateSetTest.php | 2 +- ci/phpunit/downloadapi/DownloadAppTest.php | 2 +- .../openapi/crackerbinarytype.spec.json | 30 +--- ci/phpunit/inc/UtilTest.php | 4 +- ci/phpunit/inc/utils/AccessGroupUtilsTest.php | 7 + .../inc/utils/CrackerBinaryUtilsTest.php | 41 ++++- ci/phpunit/inc/utils/CrackerUtilsTest.php | 147 +++++++++++++++++- openapi.json | 72 ++------- src/dba/models/CrackerBinary.php | 2 +- src/dba/models/CrackerBinaryType.php | 15 +- src/dba/models/CrackerBinaryTypeFactory.php | 4 +- src/dba/models/generator.php | 9 +- src/inc/apiv2/model/CrackerBinaryAPI.php | 20 ++- src/inc/apiv2/model/CrackerBinaryTypeAPI.php | 2 +- src/inc/handlers/CrackerHandler.php | 2 +- src/inc/startup/setup.json | 3 +- src/inc/user_api/UserAPICracker.php | 2 +- src/inc/utils/AccessGroupUtils.php | 8 +- src/inc/utils/CrackerBinaryUtils.php | 17 +- src/inc/utils/CrackerUtils.php | 98 +++++++++--- src/inc/utils/HashlistUtils.php | 10 +- .../20260903211245_cracker-access-groups.sql | 9 +- .../20260903211245_cracker-access-groups.sql | 10 +- 28 files changed, 359 insertions(+), 169 deletions(-) diff --git a/ci/apiv2/test_taskwrapper.py b/ci/apiv2/test_taskwrapper.py index 4cb214b6b..00d3f3a04 100644 --- a/ci/apiv2/test_taskwrapper.py +++ b/ci/apiv2/test_taskwrapper.py @@ -61,7 +61,7 @@ def test_helper_create_supertask_generic_cracker(self): version='1.2.3', downloadUrl='https://example.org/generic-1.2.3.gz', binaryName='generic-x64', - accessGroupId=crackertype.accessGroupId) + accessGroupId=1) cracker.save() self.delete_after_test(cracker) hashlist = self.create_hashlist() diff --git a/ci/apiv2/testfiles/crackertype/create_crackertype_001.json b/ci/apiv2/testfiles/crackertype/create_crackertype_001.json index 7314e24ca..9082b7f34 100644 --- a/ci/apiv2/testfiles/crackertype/create_crackertype_001.json +++ b/ci/apiv2/testfiles/crackertype/create_crackertype_001.json @@ -1,5 +1,4 @@ { "typeName": "generic2", - "isChunkingAvailable": true, - "accessGroupId": 1 + "isChunkingAvailable": true } diff --git a/ci/apiv2/testfiles/crackertype/create_crackertype_002.json b/ci/apiv2/testfiles/crackertype/create_crackertype_002.json index 0ec27d8fa..d4efcd601 100644 --- a/ci/apiv2/testfiles/crackertype/create_crackertype_002.json +++ b/ci/apiv2/testfiles/crackertype/create_crackertype_002.json @@ -1,5 +1,4 @@ { "typeName": true, - "isChunkingAvailable": true, - "accessGroupId": 1 + "isChunkingAvailable": true } diff --git a/ci/phpunit/TestBase.php b/ci/phpunit/TestBase.php index cf9d1ee0f..9709cb8b9 100644 --- a/ci/phpunit/TestBase.php +++ b/ci/phpunit/TestBase.php @@ -201,7 +201,7 @@ protected function createTaskWrapper(AccessGroup $group, Hashlist $hashlist, int protected function createCrackerBinaryType(): CrackerBinaryType { $crackerBinaryType = $this->createDatabaseObject( Factory::getCrackerBinaryTypeFactory(), - new CrackerBinaryType(null, 'type_' . uniqid(), 1, 1) + new CrackerBinaryType(null, 'type_' . uniqid(), 1) ); $this->assertTrue($crackerBinaryType instanceof CrackerBinaryType); return $crackerBinaryType; diff --git a/ci/phpunit/dba/AbstractModelFactoryTest.php b/ci/phpunit/dba/AbstractModelFactoryTest.php index 91b8ad037..f6ee9166a 100644 --- a/ci/phpunit/dba/AbstractModelFactoryTest.php +++ b/ci/phpunit/dba/AbstractModelFactoryTest.php @@ -1678,7 +1678,7 @@ private function setUpHealthCheck(): array { $hashType = new HashType(null, 'placeholder', 0, 0); $hashType = $this->createDatabaseObject(Factory::getHashTypeFactory(), $hashType); - $crackerBinaryType = new CrackerBinaryType(null, '', 0, 1); + $crackerBinaryType = new CrackerBinaryType(null, '', 0); $crackerBinaryType = $this->createDatabaseObject(Factory::getCrackerBinaryTypeFactory(), $crackerBinaryType); $crackerBinary = new CrackerBinary(null, $crackerBinaryType->getId(), '', '', '', null, 1); diff --git a/ci/phpunit/dba/MassUpdateSetTest.php b/ci/phpunit/dba/MassUpdateSetTest.php index ed9016c15..2833ae74b 100644 --- a/ci/phpunit/dba/MassUpdateSetTest.php +++ b/ci/phpunit/dba/MassUpdateSetTest.php @@ -158,7 +158,7 @@ public function testMassSingleUpdateWithMappedColumn(): void { $prefix = 'hca_' . $testId; $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, 1)); + $cbt = $this->createDatabaseObject(Factory::getCrackerBinaryTypeFactory(), new CrackerBinaryType(null, '', 0)); $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, '')); diff --git a/ci/phpunit/downloadapi/DownloadAppTest.php b/ci/phpunit/downloadapi/DownloadAppTest.php index 54e59d9ce..061a90566 100644 --- a/ci/phpunit/downloadapi/DownloadAppTest.php +++ b/ci/phpunit/downloadapi/DownloadAppTest.php @@ -39,7 +39,7 @@ protected function setUp(): void { $this->type = $this->createDatabaseObject( Factory::getCrackerBinaryTypeFactory(), - new CrackerBinaryType(null, 'download-test-type', 1, 1) + new CrackerBinaryType(null, 'download-test-type', 1) ); $this->externalBinary = $this->createDatabaseObject( Factory::getCrackerBinaryFactory(), diff --git a/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json b/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json index d39b7c744..49d61361a 100644 --- a/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json +++ b/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json @@ -1366,14 +1366,10 @@ "properties": { "typeName": { "type": "string" - }, - "accessGroupId": { - "type": "integer" } }, "required": [ - "typeName", - "accessGroupId" + "typeName" ] } } @@ -1549,8 +1545,7 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable", - "accessGroupId" + "isChunkingAvailable" ], "properties": { "typeName": { @@ -1561,9 +1556,6 @@ "boolean", "null" ] - }, - "accessGroupId": { - "type": "integer" } } }, @@ -1939,8 +1931,7 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable", - "accessGroupId" + "isChunkingAvailable" ], "properties": { "typeName": { @@ -1951,9 +1942,6 @@ "boolean", "null" ] - }, - "accessGroupId": { - "type": "integer" } } }, @@ -2329,8 +2317,7 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable", - "accessGroupId" + "isChunkingAvailable" ], "properties": { "typeName": { @@ -2341,9 +2328,6 @@ "boolean", "null" ] - }, - "accessGroupId": { - "type": "integer" } } }, @@ -2770,8 +2754,7 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable", - "accessGroupId" + "isChunkingAvailable" ], "properties": { "typeName": { @@ -2782,9 +2765,6 @@ "boolean", "null" ] - }, - "accessGroupId": { - "type": "integer" } } }, diff --git a/ci/phpunit/inc/UtilTest.php b/ci/phpunit/inc/UtilTest.php index 9332c1f06..2b4a72bf5 100644 --- a/ci/phpunit/inc/UtilTest.php +++ b/ci/phpunit/inc/UtilTest.php @@ -949,7 +949,7 @@ public function testCheckOrCreateInitialObjectAgentBinary(): void { */ public function testCheckOrCreateInitialObjectCrackerBinaryType(): void { $id = 9007; - $data = ['crackerBinaryTypeId' => $id, 'typeName' => 'testHashcat', 'isChunkingAvailable' => 1, 'accessGroupId' => 1]; + $data = ['crackerBinaryTypeId' => $id, 'typeName' => 'testHashcat', 'isChunkingAvailable' => 1]; Util::checkOrCreateInitialObject(Factory::getCrackerBinaryTypeFactory(), $data); $obj = Factory::getCrackerBinaryTypeFactory()->get($id); $this->assertNotNull($obj); @@ -965,7 +965,7 @@ public function testCheckOrCreateInitialObjectCrackerBinaryType(): void { */ public function testCheckOrCreateInitialObjectCrackerBinary(): void { $typeId = 9008; - $typeData = ['crackerBinaryTypeId' => $typeId, 'typeName' => 'binType', 'isChunkingAvailable' => 0, 'accessGroupId' => 1]; + $typeData = ['crackerBinaryTypeId' => $typeId, 'typeName' => 'binType', 'isChunkingAvailable' => 0]; Util::checkOrCreateInitialObject(Factory::getCrackerBinaryTypeFactory(), $typeData); $id = 9009; 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/CrackerBinaryUtilsTest.php b/ci/phpunit/inc/utils/CrackerBinaryUtilsTest.php index 7df4ba435..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; @@ -27,7 +31,7 @@ protected function setUp(): void { parent::setUp(); $this->type = $this->createDatabaseObject( Factory::getCrackerBinaryTypeFactory(), - new CrackerBinaryType(null, 'test-crackerbinaryutils-type', 1, 1) + new CrackerBinaryType(null, 'test-crackerbinaryutils-type', 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 2279e27d3..0af18f9f4 100644 --- a/ci/phpunit/inc/utils/CrackerUtilsTest.php +++ b/ci/phpunit/inc/utils/CrackerUtilsTest.php @@ -7,11 +7,14 @@ 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; use Hashtopolis\inc\apiv2\error\HttpError; use Hashtopolis\inc\HTException; +use Hashtopolis\inc\utils\AccessGroupUtils; +use Hashtopolis\inc\utils\AccessUtils; use Hashtopolis\inc\utils\CrackerUtils; use Hashtopolis\TestBase; @@ -35,7 +38,7 @@ protected function setUp(): void { parent::setUp(); $this->type = $this->createDatabaseObject( Factory::getCrackerBinaryTypeFactory(), - new CrackerBinaryType(null, 'test-crackerutils-type', 1, 1) + new CrackerBinaryType(null, 'test-crackerutils-type', 1) ); $this->binary = $this->createDatabaseObject( Factory::getCrackerBinaryFactory(), @@ -155,7 +158,7 @@ public function testCreateBinaryFromUploadInlineSource(): void { public function testCreateBinaryFromUploadSanitizesFilename(): void { $type = $this->createDatabaseObject( Factory::getCrackerBinaryTypeFactory(), - new CrackerBinaryType(null, 'weird cracker name!', 1, 1) + new CrackerBinaryType(null, 'weird cracker name!', 1) ); $b = CrackerUtils::createBinaryFromUpload('7.2.7', 'testcracker', $type->getId(), 'inline', base64_encode(self::SEVEN_ZIP_MAGIC)); $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $b); @@ -252,7 +255,7 @@ public function testDeleteBinaryRemovesLocalArchive(): void { public function testDeleteBinaryTypeRemovesLocalArchives(): void { $type = $this->createDatabaseObject( Factory::getCrackerBinaryTypeFactory(), - new CrackerBinaryType(null, 'type2-' . uniqid(), 1, 1) + new CrackerBinaryType(null, 'type2-' . uniqid(), 1) ); $name = 'test-archive-' . uniqid() . '.7z'; file_put_contents($this->getImportPath() . $name, self::SEVEN_ZIP_MAGIC . 'to-be-deleted'); @@ -310,4 +313,142 @@ 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 creation without an access group falls back to the first + // access group of the creating user (legacy UI and user api have no group input). + public function testCreateBinaryFallsBackToUsersFirstGroup(): void { + $group = $this->createAccessGroup('ag-crackerutils-fallback'); + $user = $this->createUser('crackerutils-fallback-user'); + // createUser already made the user a member of the default group + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $group->getId(), $user->getId()) + ); + AccessGroupUtils::removeUser($user->getId(), AccessUtils::getOrCreateDefaultAccessGroup()->getId()); + + $binary = CrackerUtils::createBinary('1.0.0', 'testcracker', 'http://example.com/hc.7z', $this->type->getId(), null, $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 cc21508a7..b42614b88 100644 --- a/openapi.json +++ b/openapi.json @@ -41940,6 +41940,9 @@ "attributes": { "type": "object", "properties": { + "accessGroupId": { + "type": "integer" + }, "binaryName": { "type": "string" }, @@ -41985,6 +41988,9 @@ "attributes": { "type": "object", "properties": { + "accessGroupId": { + "type": "integer" + }, "binaryName": { "type": "string" }, @@ -42271,8 +42277,7 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable", - "accessGroupId" + "isChunkingAvailable" ], "properties": { "typeName": { @@ -42283,9 +42288,6 @@ "boolean", "null" ] - }, - "accessGroupId": { - "type": "integer" } } } @@ -42665,8 +42667,7 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable", - "accessGroupId" + "isChunkingAvailable" ], "properties": { "typeName": { @@ -42677,9 +42678,6 @@ "boolean", "null" ] - }, - "accessGroupId": { - "type": "integer" } } } @@ -43111,8 +43109,7 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable", - "accessGroupId" + "isChunkingAvailable" ], "properties": { "typeName": { @@ -43123,9 +43120,6 @@ "boolean", "null" ] - }, - "accessGroupId": { - "type": "integer" } } } @@ -43399,14 +43393,10 @@ "properties": { "typeName": { "type": "string" - }, - "accessGroupId": { - "type": "integer" } }, "required": [ - "typeName", - "accessGroupId" + "typeName" ] } } @@ -43582,8 +43572,7 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable", - "accessGroupId" + "isChunkingAvailable" ], "properties": { "typeName": { @@ -43594,9 +43583,6 @@ "boolean", "null" ] - }, - "accessGroupId": { - "type": "integer" } } }, @@ -43972,8 +43958,7 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable", - "accessGroupId" + "isChunkingAvailable" ], "properties": { "typeName": { @@ -43984,9 +43969,6 @@ "boolean", "null" ] - }, - "accessGroupId": { - "type": "integer" } } }, @@ -44413,8 +44395,7 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable", - "accessGroupId" + "isChunkingAvailable" ], "properties": { "typeName": { @@ -44425,9 +44406,6 @@ "boolean", "null" ] - }, - "accessGroupId": { - "type": "integer" } } }, @@ -62547,8 +62525,7 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable", - "accessGroupId" + "isChunkingAvailable" ], "properties": { "typeName": { @@ -62559,9 +62536,6 @@ "boolean", "null" ] - }, - "accessGroupId": { - "type": "integer" } } } @@ -63530,8 +63504,7 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable", - "accessGroupId" + "isChunkingAvailable" ], "properties": { "typeName": { @@ -63542,9 +63515,6 @@ "boolean", "null" ] - }, - "accessGroupId": { - "type": "integer" } } } @@ -64513,8 +64483,7 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable", - "accessGroupId" + "isChunkingAvailable" ], "properties": { "typeName": { @@ -64525,9 +64494,6 @@ "boolean", "null" ] - }, - "accessGroupId": { - "type": "integer" } } } @@ -65548,8 +65514,7 @@ "type": "object", "required": [ "typeName", - "isChunkingAvailable", - "accessGroupId" + "isChunkingAvailable" ], "properties": { "typeName": { @@ -65560,9 +65525,6 @@ "boolean", "null" ] - }, - "accessGroupId": { - "type": "integer" } } } diff --git a/src/dba/models/CrackerBinary.php b/src/dba/models/CrackerBinary.php index 4ffa82139..012325e2d 100644 --- a/src/dba/models/CrackerBinary.php +++ b/src/dba/models/CrackerBinary.php @@ -44,7 +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' => True, "type" => "int", "subtype" => "unset", "choices" => "unset", "null" => False, "pk" => False, "protected" => False, "private" => False, "alias" => "accessGroupId", "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; } diff --git a/src/dba/models/CrackerBinaryType.php b/src/dba/models/CrackerBinaryType.php index 60cfa0b76..d21c3f071 100644 --- a/src/dba/models/CrackerBinaryType.php +++ b/src/dba/models/CrackerBinaryType.php @@ -8,13 +8,11 @@ class CrackerBinaryType extends AbstractModel { private ?int $crackerBinaryTypeId; private ?string $typeName; private ?int $isChunkingAvailable; - private ?int $accessGroupId; - function __construct(?int $crackerBinaryTypeId, ?string $typeName, ?int $isChunkingAvailable, ?int $accessGroupId) { + function __construct(?int $crackerBinaryTypeId, ?string $typeName, ?int $isChunkingAvailable) { $this->crackerBinaryTypeId = $crackerBinaryTypeId; $this->typeName = $typeName; $this->isChunkingAvailable = $isChunkingAvailable; - $this->accessGroupId = $accessGroupId; } function getKeyValueDict(): array { @@ -22,7 +20,6 @@ function getKeyValueDict(): array { $dict['crackerBinaryTypeId'] = $this->crackerBinaryTypeId; $dict['typeName'] = $this->typeName; $dict['isChunkingAvailable'] = $this->isChunkingAvailable; - $dict['accessGroupId'] = $this->accessGroupId; return $dict; } @@ -32,7 +29,6 @@ static function getFeatures(): array { $dict['crackerBinaryTypeId'] = ['read_only' => True, "type" => "int", "subtype" => "unset", "choices" => "unset", "null" => False, "pk" => True, "protected" => True, "private" => False, "alias" => "crackerBinaryTypeId", "public" => False, "dba_mapping" => False]; $dict['typeName'] = ['read_only' => False, "type" => "str(30)", "subtype" => "unset", "choices" => "unset", "null" => False, "pk" => False, "protected" => False, "private" => False, "alias" => "typeName", "public" => False, "dba_mapping" => False]; $dict['isChunkingAvailable'] = ['read_only' => False, "type" => "bool", "subtype" => "unset", "choices" => "unset", "null" => True, "pk" => False, "protected" => False, "private" => False, "alias" => "isChunkingAvailable", "public" => False, "dba_mapping" => False]; - $dict['accessGroupId'] = ['read_only' => True, "type" => "int", "subtype" => "unset", "choices" => "unset", "null" => False, "pk" => False, "protected" => False, "private" => False, "alias" => "accessGroupId", "public" => False, "dba_mapping" => False]; return $dict; } @@ -77,18 +73,9 @@ function setIsChunkingAvailable(?int $isChunkingAvailable): void { $this->isChunkingAvailable = $isChunkingAvailable; } - function getAccessGroupId(): ?int { - return $this->accessGroupId; - } - - function setAccessGroupId(?int $accessGroupId): void { - $this->accessGroupId = $accessGroupId; - } - const CRACKER_BINARY_TYPE_ID = "crackerBinaryTypeId"; const TYPE_NAME = "typeName"; const IS_CHUNKING_AVAILABLE = "isChunkingAvailable"; - const ACCESS_GROUP_ID = "accessGroupId"; const PERM_CREATE = "permCrackerBinaryTypeCreate"; const PERM_READ = "permCrackerBinaryTypeRead"; diff --git a/src/dba/models/CrackerBinaryTypeFactory.php b/src/dba/models/CrackerBinaryTypeFactory.php index 362f733fe..de20c9ebe 100644 --- a/src/dba/models/CrackerBinaryTypeFactory.php +++ b/src/dba/models/CrackerBinaryTypeFactory.php @@ -32,7 +32,7 @@ function getCacheValidTime(): int { * @return CrackerBinaryType */ function getNullObject(): CrackerBinaryType { - return new CrackerBinaryType(-1, null, null, null); + return new CrackerBinaryType(-1, null, null); } /** @@ -45,6 +45,6 @@ function createObjectFromDict(array $dict): CrackerBinaryType { $conv[strtolower($key)] = $val; } $dict = $conv; - return new CrackerBinaryType($dict['crackerbinarytypeid'], $dict['typename'], $dict['ischunkingavailable'], $dict['accessgroupid']); + return new CrackerBinaryType($dict['crackerbinarytypeid'], $dict['typename'], $dict['ischunkingavailable']); } } diff --git a/src/dba/models/generator.php b/src/dba/models/generator.php index 47e77484f..3c63b63e6 100644 --- a/src/dba/models/generator.php +++ b/src/dba/models/generator.php @@ -288,9 +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 of the binary, must match the access group of its cracker binary type - // (read only until the type group move including all its binaries is implemented) - ['name' => 'accessGroupId', 'read_only' => True, 'type' => 'int', 'relation' => 'AccessGroup'], + // 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'] = [ @@ -298,9 +298,6 @@ ['name' => 'crackerBinaryTypeId', 'read_only' => True, 'type' => 'int', 'protected' => True], ['name' => 'typeName', 'read_only' => False, 'type' => 'str(30)'], ['name' => 'isChunkingAvailable', 'read_only' => False, 'null' => True, 'type' => 'bool'], - // access group of the type, all its binaries are always in the same group - // (read only until the group move including all binaries is implemented) - ['name' => 'accessGroupId', 'read_only' => True, 'type' => 'int', 'relation' => 'AccessGroup'], ], ]; $CONF['File'] = [ diff --git a/src/inc/apiv2/model/CrackerBinaryAPI.php b/src/inc/apiv2/model/CrackerBinaryAPI.php index 0e5d5f51e..ab28b2563 100644 --- a/src/inc/apiv2/model/CrackerBinaryAPI.php +++ b/src/inc/apiv2/model/CrackerBinaryAPI.php @@ -8,6 +8,7 @@ use Hashtopolis\dba\models\CrackerBinary; 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; @@ -84,7 +85,8 @@ protected function createObject(array $data): int { $data[CrackerBinary::CRACKER_BINARY_TYPE_ID], $data["sourceType"], $data["sourceData"], - $data[CrackerBinary::ACCESS_GROUP_ID] ?? null + $data[CrackerBinary::ACCESS_GROUP_ID] ?? null, + $this->getCurrentUser() ); return $binary->getId(); } @@ -96,7 +98,8 @@ protected function createObject(array $data): int { $data[CrackerBinary::BINARY_NAME], $data[CrackerBinary::DOWNLOAD_URL], $data[CrackerBinary::CRACKER_BINARY_TYPE_ID], - $data[CrackerBinary::ACCESS_GROUP_ID] ?? null + $data[CrackerBinary::ACCESS_GROUP_ID] ?? null, + $this->getCurrentUser() ); return $binary->getId(); } @@ -109,6 +112,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/model/CrackerBinaryTypeAPI.php b/src/inc/apiv2/model/CrackerBinaryTypeAPI.php index 3c1bdcbe2..5beb68e87 100644 --- a/src/inc/apiv2/model/CrackerBinaryTypeAPI.php +++ b/src/inc/apiv2/model/CrackerBinaryTypeAPI.php @@ -60,7 +60,7 @@ function getAllPostParameters(array $features): array { * @throws HttpError */ protected function createObject(array $data): int { - $binaryType = CrackerUtils::createBinaryType($data[CrackerBinaryType::TYPE_NAME], $data[CrackerBinaryType::ACCESS_GROUP_ID] ?? null); + $binaryType = CrackerUtils::createBinaryType($data[CrackerBinaryType::TYPE_NAME]); return $binaryType->getId(); } diff --git a/src/inc/handlers/CrackerHandler.php b/src/inc/handlers/CrackerHandler.php index 64e3dd203..5c75475bd 100644 --- a/src/inc/handlers/CrackerHandler.php +++ b/src/inc/handlers/CrackerHandler.php @@ -32,7 +32,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'], null, AccessControl::getInstance()->getUser()); header("Location: crackers.php?id=" . $binary->getCrackerBinaryTypeId()); die(); case DCrackerBinaryAction::EDIT_BINARY: diff --git a/src/inc/startup/setup.json b/src/inc/startup/setup.json index 257b185af..2f6e72922 100644 --- a/src/inc/startup/setup.json +++ b/src/inc/startup/setup.json @@ -424,8 +424,7 @@ { "crackerBinaryTypeId" : 1, "typeName" : "hashcat", - "isChunkingAvailable" : 1, - "accessGroupId" : 1 + "isChunkingAvailable" : 1 } ], "Preprocessor" : [ diff --git a/src/inc/user_api/UserAPICracker.php b/src/inc/user_api/UserAPICracker.php index fedf9e936..adee40a20 100644 --- a/src/inc/user_api/UserAPICracker.php +++ b/src/inc/user_api/UserAPICracker.php @@ -68,7 +68,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(), null, $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/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 ccbce2df3..b94d8963e 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; @@ -37,13 +38,12 @@ public static function getBinaryTypes(): array { /** * @param string $typeName - * @param int|null $accessGroupId falls back to the default access group if not given * @return CrackerBinaryType * @throws HttpConflict * @throws HttpError * @throws Exception */ - public static function createBinaryType(string $typeName, ?int $accessGroupId = null): CrackerBinaryType { + public static function createBinaryType(string $typeName): CrackerBinaryType { $qF = new QueryFilter(CrackerBinaryType::TYPE_NAME, $typeName, "="); $check = Factory::getCrackerBinaryTypeFactory()->filter([Factory::FILTER => $qF], true); if ($check !== null) { @@ -52,13 +52,7 @@ public static function createBinaryType(string $typeName, ?int $accessGroupId = else if (strlen($typeName) == 0) { throw new HttpError("Cracker name cannot be empty!"); } - if ($accessGroupId === null) { - $accessGroupId = AccessUtils::getOrCreateDefaultAccessGroup()->getId(); - } - else if (Factory::getAccessGroupFactory()->get($accessGroupId) === null) { - throw new HttpError("Invalid access group selected!"); - } - $binaryType = new CrackerBinaryType(null, $typeName, 1, $accessGroupId); + $binaryType = new CrackerBinaryType(null, $typeName, 1); return Factory::getCrackerBinaryTypeFactory()->save($binaryType); } @@ -67,20 +61,22 @@ public static function createBinaryType(string $typeName, ?int $accessGroupId = * @param string $name * @param string $url * @param int $binaryTypeId - * @param int|null $accessGroupId must match the access group of the cracker binary type, - * falls back to the group of the type if not given + * @param int|null $accessGroupId required on creation, falls back to the first access + * group of the user (or the default group) if not given + * @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, ?int $accessGroupId = null): CrackerBinary { + public static function createBinary(string $version, string $name, string $url, int $binaryTypeId, ?int $accessGroupId = null, ?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!"); } - CrackerUtils::checkAccessGroupMatchesType($binaryType, $accessGroupId); - $binary = new CrackerBinary(null, $binaryType->getId(), $version, $url, $name, null, $binaryType->getAccessGroupId()); + $accessGroupId = CrackerUtils::resolveAccessGroupId($accessGroupId, $user); + CrackerUtils::checkUserGroupMembership($user, $accessGroupId); + $binary = new CrackerBinary(null, $binaryType->getId(), $version, $url, $name, null, $accessGroupId); return Factory::getCrackerBinaryFactory()->save($binary); } @@ -94,19 +90,21 @@ 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|null $accessGroupId must match the access group of the cracker binary type, - * falls back to the group of the type if not given + * @param int|null $accessGroupId required on creation, falls back to the first access + * group of the user (or the default group) if not given + * @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, ?int $accessGroupId = null): CrackerBinary { + public static function createBinaryFromUpload(string $version, string $name, int $binaryTypeId, string $sourceType, string $sourceData, ?int $accessGroupId = null, ?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::checkAccessGroupMatchesType($binaryType, $accessGroupId); + $accessGroupId = CrackerUtils::resolveAccessGroupId($accessGroupId, $user); + CrackerUtils::checkUserGroupMembership($user, $accessGroupId); // determine the source of the archive and validate it switch ($sourceType) { @@ -143,7 +141,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, $binaryType->getAccessGroupId()) + new CrackerBinary(null, $binaryType->getId(), $version, "", $name, null, $accessGroupId) ); $target = CrackerUtils::getCrackersPath() . $binary->getId() . '_' . $filename; @@ -300,15 +298,67 @@ public static function updateBinary(string $version, string $name, string $url, } /** - * Ensures a cracker binary is created in the access group of its type, all binaries - * of a type are always in the same group. + * Resolves the access group of a cracker binary to be created. When no group is + * given (legacy UI or user api callers without group input), the first access + * group of the creating user is used, or the default group as a last resort. * * @throws HttpError + * @throws Exception + */ + private static function resolveAccessGroupId(?int $accessGroupId, ?User $user): int { + if ($accessGroupId !== null) { + if (Factory::getAccessGroupFactory()->get($accessGroupId) === null) { + throw new HttpError("Invalid access group selected!"); + } + return $accessGroupId; + } + if ($user !== null) { + $groups = AccessUtils::getAccessGroupsOfUser($user); + if (sizeof($groups) > 0) { + return $groups[0]->getId(); + } + } + return AccessUtils::getOrCreateDefaultAccessGroup()->getId(); + } + + /** + * Ensures the user is a member of the access group, so binaries can only be + * created by members of the group they are created in. + * + * @throws HttpError + * @throws Exception */ - private static function checkAccessGroupMatchesType(CrackerBinaryType $binaryType, ?int $accessGroupId): void { - if ($accessGroupId !== null && $accessGroupId != $binaryType->getAccessGroupId()) { - throw new HttpError("The access group must match the access group of the cracker binary type!"); + private static function checkUserGroupMembership(?User $user, int $accessGroupId): void { + if ($user !== null && sizeof(AccessUtils::intersection( + array(Factory::getAccessGroupFactory()->get($accessGroupId)), 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); } /** 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/migrations/mysql/20260903211245_cracker-access-groups.sql b/src/migrations/mysql/20260903211245_cracker-access-groups.sql index 9d5feec7d..96cdb5659 100644 --- a/src/migrations/mysql/20260903211245_cracker-access-groups.sql +++ b/src/migrations/mysql/20260903211245_cracker-access-groups.sql @@ -1,13 +1,8 @@ --- Cracker binaries and types belong to an access group: all binaries of a type are in --- the same group as the type. Existing rows are assigned to the default access group. +-- 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`); -ALTER TABLE CrackerBinaryType ADD COLUMN accessGroupId int NOT NULL DEFAULT 1 AFTER isChunkingAvailable; -ALTER TABLE CrackerBinaryType ADD KEY `accessGroupId` (`accessGroupId`); -ALTER TABLE CrackerBinaryType ADD CONSTRAINT `CrackerBinaryType_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; -ALTER TABLE CrackerBinaryType ALTER accessGroupId DROP DEFAULT; diff --git a/src/migrations/postgres/20260903211245_cracker-access-groups.sql b/src/migrations/postgres/20260903211245_cracker-access-groups.sql index 4e0e139b1..0cdc649f5 100644 --- a/src/migrations/postgres/20260903211245_cracker-access-groups.sql +++ b/src/migrations/postgres/20260903211245_cracker-access-groups.sql @@ -1,14 +1,8 @@ --- Cracker binaries and types belong to an access group: all binaries of a type are in --- the same group as the type. Existing rows are assigned to the default access group. +-- 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; -ALTER TABLE CrackerBinaryType ADD COLUMN accessGroupId INT NOT NULL DEFAULT 1; - CREATE INDEX IF NOT EXISTS crackerbinary_accessgroupid_idx ON CrackerBinary(accessGroupId); -CREATE INDEX IF NOT EXISTS crackerbinarytype_accessgroupid_idx ON CrackerBinaryType(accessGroupId); - ALTER TABLE ONLY CrackerBinary ADD CONSTRAINT crackerbinary_ibfk_2 FOREIGN KEY (accessGroupId) REFERENCES AccessGroup(accessGroupId); -ALTER TABLE ONLY CrackerBinaryType ADD CONSTRAINT crackerbinarytype_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; -ALTER TABLE CrackerBinaryType ALTER COLUMN accessGroupId DROP DEFAULT; From 4aa6825af871880e51837ee1b54225e05b5badbc Mon Sep 17 00:00:00 2001 From: Sein Coray Date: Thu, 3 Sep 2026 21:55:47 +0200 Subject: [PATCH 4/5] updated the handling to force non-null values on any cracker binary creation and improved legacy handling --- ci/apiv2/test_cracker.py | 2 +- ci/phpunit/downloadapi/DownloadAppTest.php | 2 +- ci/phpunit/inc/utils/CrackerUtilsTest.php | 51 +++++++--------------- src/inc/apiv2/model/CrackerBinaryAPI.php | 4 +- src/inc/handlers/CrackerHandler.php | 3 +- src/inc/user_api/UserAPICracker.php | 3 +- src/inc/utils/CrackerUtils.php | 51 ++++++---------------- 7 files changed, 38 insertions(+), 78 deletions(-) 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/phpunit/downloadapi/DownloadAppTest.php b/ci/phpunit/downloadapi/DownloadAppTest.php index 061a90566..397a5dc92 100644 --- a/ci/phpunit/downloadapi/DownloadAppTest.php +++ b/ci/phpunit/downloadapi/DownloadAppTest.php @@ -56,7 +56,7 @@ protected function setUp(): void { $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/inc/utils/CrackerUtilsTest.php b/ci/phpunit/inc/utils/CrackerUtilsTest.php index 0af18f9f4..d1e0db7c7 100644 --- a/ci/phpunit/inc/utils/CrackerUtilsTest.php +++ b/ci/phpunit/inc/utils/CrackerUtilsTest.php @@ -13,8 +13,6 @@ use Hashtopolis\inc\apiv2\error\HttpConflict; use Hashtopolis\inc\apiv2\error\HttpError; use Hashtopolis\inc\HTException; -use Hashtopolis\inc\utils\AccessGroupUtils; -use Hashtopolis\inc\utils\AccessUtils; use Hashtopolis\inc\utils\CrackerUtils; use Hashtopolis\TestBase; @@ -92,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()); } @@ -121,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()); @@ -141,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()); @@ -160,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()); @@ -170,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 @@ -206,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) { @@ -225,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) { @@ -239,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); @@ -259,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); @@ -274,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 { @@ -295,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()); @@ -337,23 +335,6 @@ public function testCreateBinaryRequiresGroupMembership(): void { $this->assertEquals($group->getId(), $binary->getAccessGroupId()); } - // Verifies that creation without an access group falls back to the first - // access group of the creating user (legacy UI and user api have no group input). - public function testCreateBinaryFallsBackToUsersFirstGroup(): void { - $group = $this->createAccessGroup('ag-crackerutils-fallback'); - $user = $this->createUser('crackerutils-fallback-user'); - // createUser already made the user a member of the default group - $this->createDatabaseObject( - Factory::getAccessGroupUserFactory(), - new AccessGroupUser(null, $group->getId(), $user->getId()) - ); - AccessGroupUtils::removeUser($user->getId(), AccessUtils::getOrCreateDefaultAccessGroup()->getId()); - - $binary = CrackerUtils::createBinary('1.0.0', 'testcracker', 'http://example.com/hc.7z', $this->type->getId(), null, $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 { diff --git a/src/inc/apiv2/model/CrackerBinaryAPI.php b/src/inc/apiv2/model/CrackerBinaryAPI.php index ab28b2563..0c0d131b1 100644 --- a/src/inc/apiv2/model/CrackerBinaryAPI.php +++ b/src/inc/apiv2/model/CrackerBinaryAPI.php @@ -85,7 +85,7 @@ protected function createObject(array $data): int { $data[CrackerBinary::CRACKER_BINARY_TYPE_ID], $data["sourceType"], $data["sourceData"], - $data[CrackerBinary::ACCESS_GROUP_ID] ?? null, + $data[CrackerBinary::ACCESS_GROUP_ID], $this->getCurrentUser() ); return $binary->getId(); @@ -98,7 +98,7 @@ protected function createObject(array $data): int { $data[CrackerBinary::BINARY_NAME], $data[CrackerBinary::DOWNLOAD_URL], $data[CrackerBinary::CRACKER_BINARY_TYPE_ID], - $data[CrackerBinary::ACCESS_GROUP_ID] ?? null, + $data[CrackerBinary::ACCESS_GROUP_ID], $this->getCurrentUser() ); return $binary->getId(); diff --git a/src/inc/handlers/CrackerHandler.php b/src/inc/handlers/CrackerHandler.php index 5c75475bd..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'], null, AccessControl::getInstance()->getUser()); + $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/user_api/UserAPICracker.php b/src/inc/user_api/UserAPICracker.php index adee40a20..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(), null, $this->user); + 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/utils/CrackerUtils.php b/src/inc/utils/CrackerUtils.php index b94d8963e..4ab15507f 100644 --- a/src/inc/utils/CrackerUtils.php +++ b/src/inc/utils/CrackerUtils.php @@ -61,21 +61,19 @@ public static function createBinaryType(string $typeName): CrackerBinaryType { * @param string $name * @param string $url * @param int $binaryTypeId - * @param int|null $accessGroupId required on creation, falls back to the first access - * group of the user (or the default group) if not given + * @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, ?int $accessGroupId = null, ?User $user = null): 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!"); } - $accessGroupId = CrackerUtils::resolveAccessGroupId($accessGroupId, $user); - CrackerUtils::checkUserGroupMembership($user, $accessGroupId); + CrackerUtils::checkAccessGroup($accessGroupId, $user); $binary = new CrackerBinary(null, $binaryType->getId(), $version, $url, $name, null, $accessGroupId); return Factory::getCrackerBinaryFactory()->save($binary); } @@ -90,21 +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|null $accessGroupId required on creation, falls back to the first access - * group of the user (or the default group) if not given + * @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, ?int $accessGroupId = null, ?User $user = null): 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!"); } - $accessGroupId = CrackerUtils::resolveAccessGroupId($accessGroupId, $user); - CrackerUtils::checkUserGroupMembership($user, $accessGroupId); + CrackerUtils::checkAccessGroup($accessGroupId, $user); // determine the source of the archive and validate it switch ($sourceType) { @@ -298,39 +294,20 @@ public static function updateBinary(string $version, string $name, string $url, } /** - * Resolves the access group of a cracker binary to be created. When no group is - * given (legacy UI or user api callers without group input), the first access - * group of the creating user is used, or the default group as a last resort. + * 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 resolveAccessGroupId(?int $accessGroupId, ?User $user): int { - if ($accessGroupId !== null) { - if (Factory::getAccessGroupFactory()->get($accessGroupId) === null) { - throw new HttpError("Invalid access group selected!"); - } - return $accessGroupId; - } - if ($user !== null) { - $groups = AccessUtils::getAccessGroupsOfUser($user); - if (sizeof($groups) > 0) { - return $groups[0]->getId(); - } + private static function checkAccessGroup(int $accessGroupId, ?User $user): void { + $accessGroup = Factory::getAccessGroupFactory()->get($accessGroupId); + if ($accessGroup === null) { + throw new HttpError("Invalid access group selected!"); } - return AccessUtils::getOrCreateDefaultAccessGroup()->getId(); - } - - /** - * Ensures the user is a member of the access group, so binaries can only be - * created by members of the group they are created in. - * - * @throws HttpError - * @throws Exception - */ - private static function checkUserGroupMembership(?User $user, int $accessGroupId): void { if ($user !== null && sizeof(AccessUtils::intersection( - array(Factory::getAccessGroupFactory()->get($accessGroupId)), AccessUtils::getAccessGroupsOfUser($user))) == 0) { + array($accessGroup), AccessUtils::getAccessGroupsOfUser($user))) == 0) { throw new HttpError("Access group with no rights selected!"); } } From 2d382516f25fb3fcdeb57939266a7b0efed143a3 Mon Sep 17 00:00:00 2001 From: Sein Coray Date: Fri, 4 Sep 2026 07:12:23 +0200 Subject: [PATCH 5/5] access group checks completed --- ci/phpunit/TestBase.php | 4 +- ci/phpunit/downloadapi/DownloadAppTest.php | 7 +- .../openapi/crackerbinarytype.spec.json | 12 +- ci/phpunit/inc/utils/AccessUtilsTest.php | 4 +- openapi.json | 289 ++++++++++++++++-- .../agentapi/model/DownloadBinaryAction.php | 7 + .../apiv2/helper/CreateSupertaskHelperAPI.php | 3 +- src/inc/apiv2/model/CrackerBinaryAPI.php | 37 +++ src/inc/apiv2/openapi/SpecOverrides.php | 1 + .../CrackerBinaryDownloadHandler.php | 10 + src/inc/handlers/SupertaskHandler.php | 4 +- src/inc/handlers/TaskHandler.php | 5 + src/inc/user_api/UserAPITask.php | 4 +- src/inc/utils/AccessUtils.php | 13 + src/inc/utils/PretaskUtils.php | 7 +- src/inc/utils/SupertaskUtils.php | 7 +- src/inc/utils/TaskUtils.php | 7 + 17 files changed, 385 insertions(+), 36 deletions(-) diff --git a/ci/phpunit/TestBase.php b/ci/phpunit/TestBase.php index 9709cb8b9..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, 1) + 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/downloadapi/DownloadAppTest.php b/ci/phpunit/downloadapi/DownloadAppTest.php index 397a5dc92..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; @@ -48,10 +49,14 @@ protected function setUp(): void { // 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'; diff --git a/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json b/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json index 49d61361a..7a8dfe7f9 100644 --- a/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json +++ b/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json @@ -1727,7 +1727,8 @@ "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." }, "accessGroupId": { - "type": "integer" + "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." } } } @@ -2113,7 +2114,8 @@ "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." }, "accessGroupId": { - "type": "integer" + "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." } } } @@ -2499,7 +2501,8 @@ "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." }, "accessGroupId": { - "type": "integer" + "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." } } } @@ -2937,7 +2940,8 @@ "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." }, "accessGroupId": { - "type": "integer" + "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/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/openapi.json b/openapi.json index b42614b88..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" ] } ], @@ -41906,7 +41908,8 @@ "type": "string" }, "accessGroupId": { - "type": "integer" + "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": [ @@ -42132,7 +42135,8 @@ "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." }, "accessGroupId": { - "type": "integer" + "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." } } }, @@ -42151,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": [ @@ -42293,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": [ @@ -42522,7 +42605,8 @@ "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." }, "accessGroupId": { - "type": "integer" + "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." } } }, @@ -42541,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": [ @@ -42683,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": [ @@ -42963,7 +43126,8 @@ "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." }, "accessGroupId": { - "type": "integer" + "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." } } }, @@ -42982,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": [ @@ -43125,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": [ @@ -43754,7 +43997,8 @@ "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." }, "accessGroupId": { - "type": "integer" + "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." } } } @@ -44140,7 +44384,8 @@ "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." }, "accessGroupId": { - "type": "integer" + "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." } } } @@ -44578,7 +44823,8 @@ "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." }, "accessGroupId": { - "type": "integer" + "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." } } } @@ -54437,7 +54683,8 @@ "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." }, "accessGroupId": { - "type": "integer" + "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." } } } @@ -54907,7 +55154,8 @@ "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." }, "accessGroupId": { - "type": "integer" + "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." } } } @@ -55429,7 +55677,8 @@ "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." }, "accessGroupId": { - "type": "integer" + "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." } } } @@ -62499,7 +62748,8 @@ "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." }, "accessGroupId": { - "type": "integer" + "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." } } } @@ -63478,7 +63728,8 @@ "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." }, "accessGroupId": { - "type": "integer" + "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." } } } @@ -64457,7 +64708,8 @@ "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." }, "accessGroupId": { - "type": "integer" + "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." } } } @@ -65488,7 +65740,8 @@ "description": "Filename of the locally stored 7z archive, null when the binary is downloaded from the downloadUrl. Cannot be provided." }, "accessGroupId": { - "type": "integer" + "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/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 0c0d131b1..903af021c 100644 --- a/src/inc/apiv2/model/CrackerBinaryAPI.php +++ b/src/inc/apiv2/model/CrackerBinaryAPI.php @@ -2,10 +2,15 @@ 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; @@ -14,6 +19,7 @@ use Hashtopolis\inc\apiv2\error\HttpForbidden; use Hashtopolis\inc\apiv2\error\ResourceNotFoundError; use Hashtopolis\inc\HTException; +use Hashtopolis\inc\Util; /** @@ -28,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 @@ -53,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, + ], ]; } 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/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/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/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/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 }