Skip to content
Open
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
3 changes: 3 additions & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ Vrij en open source onder de EUPL-licentie.
<job>OCA\OpenRegister\BackgroundJob\CronFileTextExtractionJob</job>
<job>OCA\OpenRegister\Cron\WebhookRetryJob</job>
<job>OCA\OpenRegister\BackgroundJob\BlobMigrationJob</job>
<job>OCA\OpenRegister\BackgroundJob\DestructionCheckJob</job>
<job>OCA\OpenRegister\Cron\TransferCheckJob</job>
<job>OCA\OpenRegister\BackgroundJob\TransferExecutionJob</job>
</background-jobs>

<navigations>
Expand Down
35 changes: 35 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,16 @@
['name' => 'organisation#setActive', 'url' => '/api/organisations/{uuid}/set-active', 'verb' => 'POST'],
['name' => 'organisation#join', 'url' => '/api/organisations/{uuid}/join', 'verb' => 'POST'],
['name' => 'organisation#leave', 'url' => '/api/organisations/{uuid}/leave', 'verb' => 'POST'],

// Organisations - Tenant lifecycle management.
['name' => 'organisation#suspend', 'url' => '/api/organisations/{uuid}/suspend', 'verb' => 'PUT'],
['name' => 'organisation#activate', 'url' => '/api/organisations/{uuid}/activate', 'verb' => 'PUT'],
['name' => 'organisation#deprovision', 'url' => '/api/organisations/{uuid}/deprovision', 'verb' => 'PUT'],
['name' => 'organisation#usage', 'url' => '/api/organisations/{uuid}/usage', 'verb' => 'GET'],

// Admin - Tenant isolation verification and metrics.
['name' => 'organisation#isolationVerify', 'url' => '/api/admin/isolation-verify', 'verb' => 'POST'],
['name' => 'organisation#isolationMetrics', 'url' => '/api/admin/isolation-metrics', 'verb' => 'GET'],
// Tags.
['name' => 'tags#getAllTags', 'url' => '/api/tags', 'verb' => 'GET'],

Expand Down Expand Up @@ -516,5 +526,30 @@

// GraphQL Subscriptions (SSE).
['name' => 'graphQLSubscription#subscribe', 'url' => '/api/graphql/subscribe', 'verb' => 'GET'],

// Retention management: archival settings.
['name' => 'Settings\ConfigurationSettings#getArchivalSettings', 'url' => '/api/settings/archival', 'verb' => 'GET'],
['name' => 'Settings\ConfigurationSettings#updateArchivalSettings', 'url' => '/api/settings/archival', 'verb' => 'PUT'],
['name' => 'Settings\ConfigurationSettings#updateArchivalSettings', 'url' => '/api/settings/archival', 'verb' => 'PATCH'],

// Retention management: destruction list approval workflow.
['name' => 'retention#approveDestructionList', 'url' => '/api/retention/destruction-lists/{id}/approve', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']],
['name' => 'retention#rejectDestructionList', 'url' => '/api/retention/destruction-lists/{id}/reject', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']],

// Retention management: legal holds.
['name' => 'retention#placeLegalHold', 'url' => '/api/retention/legal-holds', 'verb' => 'POST'],
['name' => 'retention#releaseLegalHold', 'url' => '/api/retention/legal-holds/{id}', 'verb' => 'DELETE', 'requirements' => ['id' => '[^/]+']],
['name' => 'retention#placeBulkLegalHold', 'url' => '/api/retention/legal-holds/bulk', 'verb' => 'POST'],

// e-Depot transfer settings.
['name' => 'Settings\EdepotSettings#getEdepotSettings', 'url' => '/api/settings/edepot', 'verb' => 'GET'],
['name' => 'Settings\EdepotSettings#updateEdepotSettings', 'url' => '/api/settings/edepot', 'verb' => 'PUT'],
['name' => 'Settings\EdepotSettings#updateEdepotSettings', 'url' => '/api/settings/edepot', 'verb' => 'PATCH'],
['name' => 'Settings\EdepotSettings#testEdepotConnection', 'url' => '/api/settings/edepot/test', 'verb' => 'POST'],

// e-Depot transfer management.
['name' => 'transfer#index', 'url' => '/api/transfers', 'verb' => 'GET'],
['name' => 'transfer#show', 'url' => '/api/transfers/{id}', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']],
['name' => 'transfer#create', 'url' => '/api/transfers', 'verb' => 'POST'],
],
];
3 changes: 3 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,9 @@ function () {
// Register the LanguageMiddleware for Accept-Language header parsing.
$context->registerMiddleware(LanguageMiddleware::class);

// Register the TenantQuotaMiddleware for tenant quota enforcement and status checks.
$context->registerMiddleware(\OCA\OpenRegister\Middleware\TenantQuotaMiddleware::class);

// Register all services in phases to resolve circular dependencies.
$this->registerMappersWithCircularDependencies(context: $context);
$this->registerCacheAndFileHandlers(context: $context);
Expand Down
102 changes: 102 additions & 0 deletions lib/BackgroundJob/TenantDeprovisionJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

/**
* Tenant Deprovisioning Background Job
*
* Processes organisations in 'deprovisioning' state by soft-deleting their
* objects and transitioning them to 'archived' state.
*
* @category BackgroundJob
* @package OCA\OpenRegister\BackgroundJob
*
* @author Conduction Development Team <dev@conduction.nl>
* @copyright 2024 Conduction B.V.
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* @link https://OpenRegister.app
*/

declare(strict_types=1);

namespace OCA\OpenRegister\BackgroundJob;

use OCA\OpenRegister\Db\OrganisationMapper;
use OCA\OpenRegister\Service\TenantLifecycleService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use Psr\Log\LoggerInterface;

/**
* Processes deprovisioning organisations.
*
* @package OCA\OpenRegister\BackgroundJob
*/
class TenantDeprovisionJob extends TimedJob
{
/**
* Constructor
*
* @param ITimeFactory $time Time factory
* @param OrganisationMapper $organisationMapper Organisation mapper
* @param TenantLifecycleService $tenantLifecycleService Lifecycle service
* @param LoggerInterface $logger Logger
*/
public function __construct(
ITimeFactory $time,
private readonly OrganisationMapper $organisationMapper,
private readonly TenantLifecycleService $tenantLifecycleService,
private readonly LoggerInterface $logger
) {
parent::__construct(time: $time);
// Run every hour.
$this->setInterval(seconds: 3600);
}//end __construct()

/**
* Execute the background job.
*
* @param mixed $argument Job argument (unused)
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function run(mixed $argument): void
{
$this->logger->info('[TenantDeprovisionJob] Starting deprovisioning check');

try {
$organisations = $this->organisationMapper->findAll(
filters: ['status' => TenantLifecycleService::STATUS_DEPROVISIONING]

Check failure on line 70 in lib/BackgroundJob/TenantDeprovisionJob.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (psalm)

InvalidNamedArgument

lib/BackgroundJob/TenantDeprovisionJob.php:70:17: InvalidNamedArgument: Parameter $filters does not exist on function OCA\OpenRegister\Db\OrganisationMapper::findAll (see https://psalm.dev/238)

Check failure on line 70 in lib/BackgroundJob/TenantDeprovisionJob.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Unknown parameter $filters in call to method OCA\OpenRegister\Db\OrganisationMapper::findAll().
);
} catch (\Exception $e) {
$this->logger->error(
'[TenantDeprovisionJob] Failed to query deprovisioning organisations',
['error' => $e->getMessage()]
);
return;
}

foreach ($organisations as $organisation) {
try {
// Transition to archived — actual data cleanup is handled by
// the purge job after the retention period expires.
$this->tenantLifecycleService->archive($organisation);

$this->logger->info(
'[TenantDeprovisionJob] Organisation archived',
['uuid' => $organisation->getUuid()]
);
} catch (\Exception $e) {
$this->logger->error(
'[TenantDeprovisionJob] Failed to archive organisation',
['uuid' => $organisation->getUuid(), 'error' => $e->getMessage()]
);
}
}

$this->logger->info(
'[TenantDeprovisionJob] Completed, processed '.count($organisations).' organisations'
);
}//end run()
}//end class
138 changes: 138 additions & 0 deletions lib/BackgroundJob/TenantPurgeJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

/**
* Tenant Purge Background Job
*
* Permanently deletes archived organisations and their data after the
* configured retention period (default: 90 days).
*
* @category BackgroundJob
* @package OCA\OpenRegister\BackgroundJob
*
* @author Conduction Development Team <dev@conduction.nl>
* @copyright 2024 Conduction B.V.
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* @link https://OpenRegister.app
*/

declare(strict_types=1);

namespace OCA\OpenRegister\BackgroundJob;

use DateTime;
use DateInterval;
use OCA\OpenRegister\Db\OrganisationMapper;
use OCA\OpenRegister\Db\TenantUsageMapper;
use OCA\OpenRegister\Service\TenantLifecycleService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\IAppConfig;
use Psr\Log\LoggerInterface;

/**
* Purges archived organisations after retention period.
*
* @package OCA\OpenRegister\BackgroundJob
*/
class TenantPurgeJob extends TimedJob
{
/**
* Default retention period in days.
*/
private const DEFAULT_RETENTION_DAYS = 90;

/**
* Constructor
*
* @param ITimeFactory $time Time factory
* @param OrganisationMapper $organisationMapper Organisation mapper
* @param TenantUsageMapper $tenantUsageMapper Usage mapper
* @param IAppConfig $appConfig App config
* @param LoggerInterface $logger Logger
*/
public function __construct(
ITimeFactory $time,
private readonly OrganisationMapper $organisationMapper,
private readonly TenantUsageMapper $tenantUsageMapper,
private readonly IAppConfig $appConfig,
private readonly LoggerInterface $logger
) {
parent::__construct(time: $time);
// Run daily.
$this->setInterval(seconds: 86400);
}//end __construct()

/**
* Execute the background job.
*
* @param mixed $argument Job argument (unused)
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function run(mixed $argument): void
{
$this->logger->info('[TenantPurgeJob] Starting purge check');

$retentionDays = (int) $this->appConfig->getValueString(
'openregister',
'tenantRetentionDays',
(string) self::DEFAULT_RETENTION_DAYS
);

$cutoffDate = new DateTime();
$cutoffDate->sub(new DateInterval("P{$retentionDays}D"));

try {
$organisations = $this->organisationMapper->findAll(
filters: ['status' => TenantLifecycleService::STATUS_ARCHIVED]

Check failure on line 90 in lib/BackgroundJob/TenantPurgeJob.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (psalm)

InvalidNamedArgument

lib/BackgroundJob/TenantPurgeJob.php:90:17: InvalidNamedArgument: Parameter $filters does not exist on function OCA\OpenRegister\Db\OrganisationMapper::findAll (see https://psalm.dev/238)

Check failure on line 90 in lib/BackgroundJob/TenantPurgeJob.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Unknown parameter $filters in call to method OCA\OpenRegister\Db\OrganisationMapper::findAll().
);
} catch (\Exception $e) {
$this->logger->error(
'[TenantPurgeJob] Failed to query archived organisations',
['error' => $e->getMessage()]
);
return;
}

$purgedCount = 0;
foreach ($organisations as $organisation) {
$deprovisionedAt = $organisation->getDeprovisionedAt();
if ($deprovisionedAt === null) {
continue;
}

if ($deprovisionedAt > $cutoffDate) {
continue;
}

try {
$orgUuid = $organisation->getUuid();

// Delete usage records for this organisation.
$this->tenantUsageMapper->deleteOlderThan(new DateTime('2099-12-31'));

// Delete the organisation entity.
$this->organisationMapper->delete($organisation);

$this->logger->info(
'[TenantPurgeJob] Permanently deleted archived organisation',
['uuid' => $orgUuid, 'deprovisionedAt' => $deprovisionedAt->format('c')]
);

$purgedCount++;
} catch (\Exception $e) {
$this->logger->error(
'[TenantPurgeJob] Failed to purge organisation',
['uuid' => $organisation->getUuid(), 'error' => $e->getMessage()]
);
}//end try
}//end foreach

$this->logger->info(
'[TenantPurgeJob] Completed, purged '.$purgedCount.' organisations'
);
}//end run()
}//end class
Loading
Loading