diff --git a/src/Controller/Organization/PackageController.php b/src/Controller/Organization/PackageController.php index 6cc2609a..0c4b31e0 100644 --- a/src/Controller/Organization/PackageController.php +++ b/src/Controller/Organization/PackageController.php @@ -209,7 +209,8 @@ private function packageNewFromUrl(string $label, FormInterface $form, Organizat $form->get('url')->getData(), in_array($type, ['git', 'mercurial', 'subversion'], true) ? 'vcs' : $type, [], - $form->get('keepLastReleases')->getData() + $form->get('keepLastReleases')->getData(), + $organization->isSecurityScanEnabled() )); $this->messageBus->dispatch(new SynchronizePackage($id)); @@ -238,7 +239,8 @@ private function packageNewFromGitHub(FormInterface $form, Organization $organiz "https://github.com/{$repo}", 'github-oauth', [Metadata::GITHUB_REPO_NAME => $repo], - $form->get('keepLastReleases')->getData() + $form->get('keepLastReleases')->getData(), + $organization->isSecurityScanEnabled() )); $this->messageBus->dispatch(new SynchronizePackage($id)); $this->messageBus->dispatch(new AddGitHubHook($id)); @@ -269,7 +271,8 @@ private function packageNewFromGitLab(FormInterface $form, Organization $organiz $projects->get($projectId)->url(), 'gitlab-oauth', [Metadata::GITLAB_PROJECT_ID => $projectId], - $form->get('keepLastReleases')->getData() + $form->get('keepLastReleases')->getData(), + $organization->isSecurityScanEnabled() )); $this->messageBus->dispatch(new SynchronizePackage($id)); $this->messageBus->dispatch(new AddGitLabHook($id)); @@ -300,7 +303,8 @@ private function packageNewFromBitbucket(FormInterface $form, Organization $orga $repos->get($repoUuid)->url(), 'bitbucket-oauth', [Metadata::BITBUCKET_REPO_NAME => $repos->get($repoUuid)->name()], - $form->get('keepLastReleases')->getData() + $form->get('keepLastReleases')->getData(), + $organization->isSecurityScanEnabled() )); $this->messageBus->dispatch(new SynchronizePackage($id)); $this->messageBus->dispatch(new AddBitbucketHook($id)); diff --git a/src/Controller/OrganizationController.php b/src/Controller/OrganizationController.php index 094457d4..18903903 100644 --- a/src/Controller/OrganizationController.php +++ b/src/Controller/OrganizationController.php @@ -7,10 +7,12 @@ use Buddy\Repman\Form\Type\Organization\ChangeAliasType; use Buddy\Repman\Form\Type\Organization\ChangeAnonymousAccessType; use Buddy\Repman\Form\Type\Organization\ChangeNameType; +use Buddy\Repman\Form\Type\Organization\EnableSecurityScanType; use Buddy\Repman\Form\Type\Organization\GenerateTokenType; use Buddy\Repman\Message\Organization\ChangeAlias; use Buddy\Repman\Message\Organization\ChangeAnonymousAccess; use Buddy\Repman\Message\Organization\ChangeName; +use Buddy\Repman\Message\Organization\ChangeSecurityScanConfiguration; use Buddy\Repman\Message\Organization\GenerateToken; use Buddy\Repman\Message\Organization\Package\AddBitbucketHook; use Buddy\Repman\Message\Organization\Package\AddGitHubHook; @@ -290,11 +292,21 @@ public function settings(Organization $organization, Request $request): Response return $this->redirectToRoute('organization_settings', ['organization' => $organization->alias()]); } + $enableSecurityScanForm = $this->createForm(EnableSecurityScanType::class, ['isSecurityScanEnabled' => $organization->isSecurityScanEnabled()]); + $enableSecurityScanForm->handleRequest($request); + if ($enableSecurityScanForm->isSubmitted() && $enableSecurityScanForm->isValid()) { + $this->messageBus->dispatch(new ChangeSecurityScanConfiguration($organization->id(), $enableSecurityScanForm->get('isSecurityScanEnabled')->getData())); + $this->addFlash('success', 'Default package security scans have been successfully changed.'); + + return $this->redirectToRoute('organization_settings', ['organization' => $organization->alias()]); + } + return $this->render('organization/settings.html.twig', [ 'organization' => $organization, 'renameForm' => $renameForm->createView(), 'aliasForm' => $aliasForm->createView(), 'anonymousAccessForm' => $anonymousAccessForm->createView(), + 'enableSecurityScanForm' => $enableSecurityScanForm->createView(), ]); } diff --git a/src/Entity/Organization.php b/src/Entity/Organization.php index 7b7b70d9..6d9c2282 100644 --- a/src/Entity/Organization.php +++ b/src/Entity/Organization.php @@ -70,6 +70,11 @@ class Organization */ private bool $hasAnonymousAccess = false; + /** + * @ORM\Column(type="boolean", options={"default": true}) + */ + private bool $enableSecurityScan = true; + public function __construct(UuidInterface $id, User $owner, string $name, string $alias) { $this->id = $id; @@ -256,6 +261,11 @@ public function changeAnonymousAccess(bool $hasAnonymousAccess): void $this->hasAnonymousAccess = $hasAnonymousAccess; } + public function enableSecurityScan(bool $enableSecurityScan): void + { + $this->enableSecurityScan = $enableSecurityScan; + } + private function isLastOwner(User $user): bool { $owners = $this->members->filter(fn (Member $member) => $member->isOwner()); diff --git a/src/Form/Type/Organization/EnableSecurityScanType.php b/src/Form/Type/Organization/EnableSecurityScanType.php new file mode 100644 index 00000000..bf88bf28 --- /dev/null +++ b/src/Form/Type/Organization/EnableSecurityScanType.php @@ -0,0 +1,36 @@ + $options + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('isSecurityScanEnabled', ChoiceType::class, [ + 'choices' => [ + 'Enable' => true, + 'Disable' => false, + ], + 'label' => 'Enable security scan for new packages', + 'required' => true, + ]) + ->add('enableSecurityScan', SubmitType::class, ['label' => 'Change']) + ; + } +} diff --git a/src/Message/Organization/AddPackage.php b/src/Message/Organization/AddPackage.php index 6e44afb7..7a88e906 100644 --- a/src/Message/Organization/AddPackage.php +++ b/src/Message/Organization/AddPackage.php @@ -11,6 +11,7 @@ final class AddPackage private string $type; private string $organizationId; private int $keepLastReleases; + private bool $enableSecurityScan; /** * @var mixed[] @@ -20,7 +21,7 @@ final class AddPackage /** * @param mixed[] $metadata */ - public function __construct(string $id, string $organizationId, string $url, string $type = 'vcs', array $metadata = [], ?int $keepLastReleases = null) + public function __construct(string $id, string $organizationId, string $url, string $type = 'vcs', array $metadata = [], ?int $keepLastReleases = null, bool $enableSecurityScan = true) { $this->id = $id; $this->organizationId = $organizationId; @@ -28,6 +29,7 @@ public function __construct(string $id, string $organizationId, string $url, str $this->type = $type; $this->metadata = $metadata; $this->keepLastReleases = $keepLastReleases ?? 0; + $this->enableSecurityScan = $enableSecurityScan; } public function id(): string @@ -62,4 +64,9 @@ public function keepLastReleases(): int { return $this->keepLastReleases; } + + public function hasSecurityScanEnabled(): bool + { + return $this->enableSecurityScan; + } } diff --git a/src/Message/Organization/ChangeSecurityScanConfiguration.php b/src/Message/Organization/ChangeSecurityScanConfiguration.php new file mode 100644 index 00000000..5716adb4 --- /dev/null +++ b/src/Message/Organization/ChangeSecurityScanConfiguration.php @@ -0,0 +1,27 @@ +organizationId = $organizationId; + $this->enableSecurityScan = $enableSecurityScan; + } + + public function organizationId(): string + { + return $this->organizationId; + } + + public function hasSecurityScanEnabled(): bool + { + return $this->enableSecurityScan; + } +} diff --git a/src/MessageHandler/Organization/AddPackageHandler.php b/src/MessageHandler/Organization/AddPackageHandler.php index ba5e6ba7..5843ba5d 100644 --- a/src/MessageHandler/Organization/AddPackageHandler.php +++ b/src/MessageHandler/Organization/AddPackageHandler.php @@ -29,7 +29,8 @@ public function __invoke(AddPackage $message): void $message->type(), $message->url(), $message->metadata(), - $message->keepLastReleases() + $message->keepLastReleases(), + $message->hasSecurityScanEnabled() ) ) ; diff --git a/src/MessageHandler/Organization/EnableSecurityScanHandler.php b/src/MessageHandler/Organization/EnableSecurityScanHandler.php new file mode 100644 index 00000000..401462d9 --- /dev/null +++ b/src/MessageHandler/Organization/EnableSecurityScanHandler.php @@ -0,0 +1,28 @@ +repositories = $repositories; + } + + public function __invoke(ChangeSecurityScanConfiguration $message): void + { + $this->repositories + ->getById(Uuid::fromString($message->organizationId())) + ->enableSecurityScan($message->hasSecurityScanEnabled()) + ; + } +} diff --git a/src/Migrations/Version20220520191545.php b/src/Migrations/Version20220520191545.php new file mode 100644 index 00000000..529f477b --- /dev/null +++ b/src/Migrations/Version20220520191545.php @@ -0,0 +1,28 @@ +addSql('ALTER TABLE organization ADD enable_security_scan BOOLEAN DEFAULT \'true\' NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE organization DROP enable_security_scan'); + } +} diff --git a/src/Query/Api/Model/Organization.php b/src/Query/Api/Model/Organization.php index 33c0443f..67e4fdee 100644 --- a/src/Query/Api/Model/Organization.php +++ b/src/Query/Api/Model/Organization.php @@ -10,13 +10,15 @@ final class Organization implements \JsonSerializable private string $name; private string $alias; private bool $hasAnonymousAccess; + private bool $enableSecurityScan; - public function __construct(string $id, string $name, string $alias, bool $hasAnonymousAccess) + public function __construct(string $id, string $name, string $alias, bool $hasAnonymousAccess, bool $enableSecurityScan) { $this->id = $id; $this->name = $name; $this->alias = $alias; $this->hasAnonymousAccess = $hasAnonymousAccess; + $this->enableSecurityScan = $enableSecurityScan; } public function getId(): string @@ -39,6 +41,11 @@ public function getHasAnonymousAccess(): bool return $this->hasAnonymousAccess; } + public function getEnabledSecurityScan(): bool + { + return $this->enableSecurityScan; + } + /** * @return array */ @@ -49,6 +56,7 @@ public function jsonSerialize(): array 'name' => $this->getName(), 'alias' => $this->getAlias(), 'hasAnonymousAccess' => $this->getHasAnonymousAccess(), + 'enabledSecurityScan' => $this->enableSecurityScan, ]; } } diff --git a/src/Query/Api/OrganizationQuery/DbalOrganizationQuery.php b/src/Query/Api/OrganizationQuery/DbalOrganizationQuery.php index 56998155..8614c524 100644 --- a/src/Query/Api/OrganizationQuery/DbalOrganizationQuery.php +++ b/src/Query/Api/OrganizationQuery/DbalOrganizationQuery.php @@ -25,7 +25,7 @@ public function __construct(Connection $connection) public function getById(string $id): Option { $data = $this->connection->fetchAssociative( - 'SELECT id, name, alias, has_anonymous_access + 'SELECT id, name, alias, has_anonymous_access, enable_security_scan FROM "organization" WHERE id = :id', [ 'id' => $id, ]); @@ -41,7 +41,7 @@ public function getUserOrganizations(string $userId, int $limit = 20, int $offse return array_map(function (array $data): Organization { return $this->hydrateOrganization($data); }, $this->connection->fetchAllAssociative( - 'SELECT o.id, o.name, o.alias, om.role, o.has_anonymous_access + 'SELECT o.id, o.name, o.alias, om.role, o.has_anonymous_access, o.enable_security_scan FROM organization_member om JOIN organization o ON o.id = om.organization_id WHERE om.user_id = :userId @@ -143,6 +143,7 @@ private function hydrateOrganization(array $data): Organization $data['name'], $data['alias'], $data['has_anonymous_access'], + $data['enable_security_scan'], ); } diff --git a/src/Query/User/Model/Organization.php b/src/Query/User/Model/Organization.php index c361b6a3..e86dd446 100644 --- a/src/Query/User/Model/Organization.php +++ b/src/Query/User/Model/Organization.php @@ -13,6 +13,7 @@ final class Organization private string $name; private string $alias; private bool $hasAnonymousAccess; + private bool $enableSecurityScan; /** * @var Member[] @@ -22,13 +23,14 @@ final class Organization /** * @param Member[] $members */ - public function __construct(string $id, string $name, string $alias, array $members, bool $hasAnonymousAccess) + public function __construct(string $id, string $name, string $alias, array $members, bool $hasAnonymousAccess, bool $enableSecurityScan) { $this->id = $id; $this->name = $name; $this->alias = $alias; $this->members = array_map(fn (Member $member) => $member, $members); $this->hasAnonymousAccess = $hasAnonymousAccess; + $this->enableSecurityScan = $enableSecurityScan; } public function id(): string @@ -93,4 +95,9 @@ public function hasAnonymousAccess(): bool { return $this->hasAnonymousAccess; } + + public function isSecurityScanEnabled(): bool + { + return $this->enableSecurityScan; + } } diff --git a/src/Query/User/OrganizationQuery/DbalOrganizationQuery.php b/src/Query/User/OrganizationQuery/DbalOrganizationQuery.php index a7638539..a9bfbe48 100644 --- a/src/Query/User/OrganizationQuery/DbalOrganizationQuery.php +++ b/src/Query/User/OrganizationQuery/DbalOrganizationQuery.php @@ -29,7 +29,7 @@ public function __construct(Connection $connection) public function getByAlias(string $alias): Option { $data = $this->connection->fetchAssociative( - 'SELECT id, name, alias, has_anonymous_access + 'SELECT id, name, alias, has_anonymous_access, enable_security_scan FROM "organization" WHERE alias = :alias', [ 'alias' => $alias, ]); @@ -44,7 +44,7 @@ public function getByAlias(string $alias): Option public function getByInvitation(string $token, string $email): Option { $data = $this->connection->fetchAssociative( - 'SELECT o.id, o.name, o.alias, o.has_anonymous_access + 'SELECT o.id, o.name, o.alias, o.has_anonymous_access, o.enable_security_scan FROM "organization" o JOIN organization_invitation i ON o.id = i.organization_id WHERE i.token = :token AND i.email = :email @@ -240,6 +240,7 @@ private function hydrateOrganization(array $data): Organization $data['alias'], array_map(fn (array $row) => new Member($row['user_id'], $row['email'], $row['role']), $members), $data['has_anonymous_access'], + $data['enable_security_scan'], ); } diff --git a/templates/organization/settings.html.twig b/templates/organization/settings.html.twig index 38e2ee05..4ac72764 100644 --- a/templates/organization/settings.html.twig +++ b/templates/organization/settings.html.twig @@ -47,6 +47,21 @@ {{ form_end(anonymousAccessForm) }}
+

Package Security scan

+

+ Enable or disable the security scan for new packages. Changing this setting will not affect existing packages! +

+ {{ form_start(enableSecurityScanForm, {attr:{class:'row'}}) }} +
+ {{ form_widget(enableSecurityScanForm.isSecurityScanEnabled) }} + {{ form_errors(enableSecurityScanForm.isSecurityScanEnabled) }} +
+
+ {{ form_widget(enableSecurityScanForm.enableSecurityScan, {attr:{class:'btn-danger'}}) }} +
+ {{ form_end(enableSecurityScanForm) }} +
+

Delete this organization

diff --git a/tests/Functional/Controller/Api/OrganizationControllerTest.php b/tests/Functional/Controller/Api/OrganizationControllerTest.php index 0abdf5ac..86648ed2 100644 --- a/tests/Functional/Controller/Api/OrganizationControllerTest.php +++ b/tests/Functional/Controller/Api/OrganizationControllerTest.php @@ -39,6 +39,7 @@ public function testOrganizationsList(): void self::assertEquals($json['data'][0]['name'], 'Buddy works'); self::assertEquals($json['data'][0]['alias'], 'buddy-works'); self::assertEquals($json['data'][0]['hasAnonymousAccess'], false); + self::assertEquals($json['data'][0]['enabledSecurityScan'], true); self::assertEquals($json['total'], 1); self::assertNotEmpty($json['links']); } @@ -83,6 +84,7 @@ public function testCreateOrganization(): void self::assertEquals($json['name'], 'New organization'); self::assertEquals($json['alias'], 'new-organization'); self::assertEquals($json['hasAnonymousAccess'], false); + self::assertEquals($json['enabledSecurityScan'], true); self::assertFalse( $this->container() ->get(DbalOrganizationQuery::class) diff --git a/tests/Functional/Controller/OrganizationControllerTest.php b/tests/Functional/Controller/OrganizationControllerTest.php index 65c4f4fb..1b1423c2 100644 --- a/tests/Functional/Controller/OrganizationControllerTest.php +++ b/tests/Functional/Controller/OrganizationControllerTest.php @@ -750,6 +750,35 @@ public function testChangeAnonymousAccess(): void self::assertStringContainsString('Anonymous access has been successfully changed.', $this->lastResponseBody()); } + public function testChangeEnableSecurityScan(): void + { + $this->fixtures->createOrganization('buddy', $this->userId); + $this->client->followRedirects(); + + $organization = $this + ->container() + ->get(DbalOrganizationQuery::class) + ->getByAlias('buddy') + ->get(); + + self::assertTrue($organization->isSecurityScanEnabled()); + + $this->client->request('GET', $this->urlTo('organization_settings', ['organization' => 'buddy'])); + $this->client->submitForm('enableSecurityScan', [ + 'isSecurityScanEnabled' => 0, + ]); + + $organization = $this + ->container() + ->get(DbalOrganizationQuery::class) + ->getByAlias('buddy') + ->get(); + + self::assertFalse($organization->isSecurityScanEnabled()); + self::assertTrue($this->client->getResponse()->isOk()); + self::assertStringContainsString('Default package security scans have been successfully changed.', $this->lastResponseBody()); + } + public function testRemoveOrganization(): void { $organizationId = $this->fixtures->createOrganization('buddy inc', $this->userId); diff --git a/tests/MotherObject/Query/OrganizationMother.php b/tests/MotherObject/Query/OrganizationMother.php index f9f222f2..a4d2f9ca 100644 --- a/tests/MotherObject/Query/OrganizationMother.php +++ b/tests/MotherObject/Query/OrganizationMother.php @@ -16,7 +16,8 @@ public static function some(): Organization 'Repman', 'repman', [new Organization\Member('5c1b8e35-fe7b-4418-b722-ec9cbbf2598a', 'test@repman.io', 'owner')], - false + false, + true ); } @@ -27,7 +28,8 @@ public static function withMember(Member $member): Organization 'Repman', 'repman', [$member], - false + false, + true ); } }