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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use OCA\OpenRegister\Event\ObjectCreatingEvent;
use OCA\OpenRegister\Event\ObjectDeletedEvent;
use OCA\OpenRegister\Event\ObjectUpdatedEvent;
use OCA\OpenRegister\Event\ObjectUpdatingEvent;
use OCA\OpenRegister\Event\UserProfileUpdatedEvent;
use OCA\OpenRegister\Service\OrganisationService as OpenRegisterOrganisationService;
use OCA\Stackiq\BackgroundJob\ContractStatusJob;
Expand All @@ -38,6 +39,7 @@
use OCA\Stackiq\EventListener\ModuleVersionPublicationListener;
use OCA\Stackiq\EventListener\ModuleComplianceSubscriber;
use OCA\Stackiq\EventListener\AnonymousRegistrationListener;
use OCA\Stackiq\EventListener\ContactPersonOrganisationListener;
use OCA\Stackiq\EventListener\ModuleRegistrationSubscriber;
use OCA\Stackiq\EventListener\TestEventListener;
use OCA\Stackiq\EventListener\UserProfileUpdatedEventListener;
Expand Down Expand Up @@ -818,6 +820,10 @@ private function registerEventListeners(IRegistrationContext $context): void {

// An organisation registered without an account stays pending until reviewed.
$context->registerEventListener(ObjectCreatingEvent::class, AnonymousRegistrationListener::class);

// A contact person only from someone who may add members to its organisation.
$context->registerEventListener(ObjectCreatingEvent::class, ContactPersonOrganisationListener::class);
$context->registerEventListener(ObjectUpdatingEvent::class, ContactPersonOrganisationListener::class);
$context->registerEventListener(ObjectUpdatedEvent::class, ModuleRegistrationSubscriber::class);

// Queue the owner resolution when a supplier announces maintenance (lifecycle-maintenance-and-supplier-roadmap).
Expand Down
14 changes: 13 additions & 1 deletion lib/BackgroundJob/OrganizationContactSyncJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

namespace OCA\Stackiq\BackgroundJob;

use OCA\Stackiq\Service\Contactpersoon\SystemContactWrites;
use OCA\Stackiq\Service\OrganizationSyncService;
use OCA\Stackiq\Service\SettingsService;
use OCA\Stackiq\Service\StackiqContactSyncService;
Expand Down Expand Up @@ -104,6 +105,17 @@ public function __construct(
* @spec openspec/specs/softwarecatalog-contacts-to-nc/spec.md
*/
protected function run($argument): void {
(new SystemContactWrites())->run(fn () => $this->runSync());
}//end run()

/**
* The scheduled sync and link refresh.
*
* @return void
*
* @spec openspec/specs/softwarecatalog-contacts-to-nc/spec.md
*/
private function runSync(): void {
if (in_array('openregister', $this->appManager->getInstalledApps(), true) === false) {
$this->logger->info('[OrganizationContactSyncJob] OpenRegister not installed, skipping sync');
return;
Expand Down Expand Up @@ -143,7 +155,7 @@ protected function run($argument): void {
]
);
}//end try
}//end run()
}//end runSync()

/**
* Refresh the `contactsUid` link on every contactpersoon/organisatie record.
Expand Down
107 changes: 107 additions & 0 deletions lib/EventListener/ContactPersonOrganisationListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

/**
* Refuses a contact person written by someone who may not add members to its organisation.
*
* @category EventListener
* @package OCA\Stackiq\EventListener
* @author Conduction b.v. <info@conduction.nl>
* @copyright 2026 Conduction B.V.
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
* @link https://github.com/ConductionNL/stackiq
*
* @spec openspec/specs/contactpersoon-sync/spec.md#requirement-the-system-shall-only-accept-a-contact-person-from-someone-who-may-add-members-to-its-organisation-req-003
*
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
* SPDX-License-Identifier: EUPL-1.2
*/

declare(strict_types=1);

namespace OCA\Stackiq\EventListener;

use OCA\OpenRegister\Db\ObjectEntity;
use OCA\OpenRegister\Event\ObjectCreatingEvent;
use OCA\OpenRegister\Event\ObjectUpdatingEvent;
use OCA\Stackiq\Service\Contactpersoon\ContactPersonOrganisationGuard;
use OCA\Stackiq\Service\Contactpersoon\SystemContactWrites;
use OCA\Stackiq\Service\SettingsService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;

/**
* On OpenRegister's creating and updating events for a contact person: the write is
* refused unless {@see ContactPersonOrganisationGuard} allows it. An update that keeps
* every key the guard reads is not checked again. Writes stackiq's own contact sync
* and migration make, marked through {@see SystemContactWrites}, are not checked.
* Other command line writes, such as OpenRegister bulk jobs, are.
*
* @template-implements IEventListener<Event>
*
* @spec openspec/specs/contactpersoon-sync/spec.md#requirement-the-system-shall-only-accept-a-contact-person-from-someone-who-may-add-members-to-its-organisation-req-003
*/
class ContactPersonOrganisationListener implements IEventListener {
/**
* Constructor.
*
* @param ContactPersonOrganisationGuard $guard Decides whether the caller may write the contact person.
* @param SettingsService $settingsService Names the contact person schema.
* @param SystemContactWrites $systemWrites Tells whether stackiq itself is writing.
*/
public function __construct(
private readonly ContactPersonOrganisationGuard $guard,
private readonly SettingsService $settingsService,
private readonly SystemContactWrites $systemWrites,
) {
}//end __construct()

/**
* Refuse a contact person write the caller may not make.
*
* @param Event $event The dispatched event.
*
* @return void
*
* @spec openspec/specs/contactpersoon-sync/spec.md#requirement-the-system-shall-only-accept-a-contact-person-from-someone-who-may-add-members-to-its-organisation-req-003
*/
public function handle(Event $event): void {
if ($this->systemWrites->isActive() === true) {
return;
}

if ($event instanceof ObjectCreatingEvent) {
$this->check(event: $event, object: $event->getObject(), oldData: null);
return;
}

if ($event instanceof ObjectUpdatingEvent) {
$this->check(event: $event, object: $event->getNewObject(), oldData: $event->getOldObject()?->getObject());
}
}//end handle()

/**
* Stop the write when it is a contact person the guard does not allow.
*
* @param ObjectCreatingEvent|ObjectUpdatingEvent $event The pre-save event.
* @param ObjectEntity $object The object being written.
* @param array<string, mixed>|null $oldData The stored data, or null on a create.
*
* @return void
*/
private function check(ObjectCreatingEvent|ObjectUpdatingEvent $event, ObjectEntity $object, ?array $oldData): void {
$configured = (string)($this->settingsService->getVoorzieningenConfig()['contactpersoon_schema'] ?? '');
if ($configured === '' || (string)$object->getSchema() !== $configured) {
return;
}

$data = array_merge(($object->getObject() ?? []), $event->getModifiedData());
if ($this->guard->needsCheck(contactData: $data, oldData: $oldData) === false
|| $this->guard->allows(contactData: $data, oldData: $oldData) === true
) {
return;
}

$event->setErrors(['message' => 'This contact person could not be saved.']);
$event->stopPropagation();
}//end check()
}//end class
16 changes: 15 additions & 1 deletion lib/Repair/MigrateContactsToNc.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

namespace OCA\Stackiq\Repair;

use OCA\Stackiq\Service\Contactpersoon\SystemContactWrites;
use OCA\Stackiq\AppInfo\AppIdentity;
use OCA\Stackiq\Service\SettingsService;
use OCA\Stackiq\Service\StackiqContactSyncService;
Expand Down Expand Up @@ -105,6 +106,19 @@ public function getName(): string {
* @spec openspec/specs/softwarecatalog-contacts-to-nc/spec.md
*/
public function run(IOutput $output): void {
(new SystemContactWrites())->run(fn () => $this->migrate(output: $output));
}//end run()

/**
* Migrate the contacts.
*
* @param IOutput $output The output interface for progress reporting.
*
* @return void
*
* @spec openspec/specs/softwarecatalog-contacts-to-nc/spec.md
*/
private function migrate(IOutput $output): void {
$output->startProgress(2);

// Converged on a previous run — skip quietly. This step is registered
Expand Down Expand Up @@ -169,7 +183,7 @@ public function run(IOutput $output): void {
}

$output->finishProgress();
}//end run()
}//end migrate()

/**
* Migrate every object of one type.
Expand Down
Loading
Loading