Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.
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
61 changes: 59 additions & 2 deletions lib/Controller/AcController.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
namespace OCA\Procest\Controller;

use OCA\Procest\Service\ZgwService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IL10N;
Expand All @@ -52,7 +51,7 @@
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
class AcController extends Controller
class AcController extends ZgwController
{
/**
* Constructor.
Expand Down Expand Up @@ -177,6 +176,20 @@ public function create(): JSONResponse
return $authError;
}

// C2: Gate writes on ac.aanmaken scope.
if ($this->zgwService->consumerHasScope($this->request, 'ac', 'ac.aanmaken') === false) {
return new JSONResponse(
data: [
'type' => 'PermissionDenied',
'code' => 'permission_denied',
'title' => 'U heeft geen toestemming om autorisaties aan te maken.',
'status' => Http::STATUS_FORBIDDEN,
'detail' => 'Scope ac.aanmaken is vereist.',
],
statusCode: Http::STATUS_FORBIDDEN
);
}

if ($this->zgwService->getConsumerMapper() === null) {
return new JSONResponse(
data: ['detail' => 'Consumer service not available'],
Expand All @@ -187,6 +200,22 @@ public function create(): JSONResponse
try {
$body = $this->zgwService->getRequestBody($this->request);

// C2: Block setting superuser=true unless the requesting consumer is itself a superuser.
if (($body['heeftAlleAutorisaties'] ?? false) === true
&& $this->zgwService->consumerHasScope($this->request, 'ac', 'ac.superuser') === false
) {
return new JSONResponse(
data: [
'invalidParams' => [[
'name' => 'heeftAlleAutorisaties',
'code' => 'permission_denied',
'reason' => 'Alleen superuser-consumers mogen heeftAlleAutorisaties op true zetten.',
]],
],
statusCode: Http::STATUS_FORBIDDEN
);
}

// Run AC business rules validation.
$validationError = $this->validateApplicatieBody(body: $body);
if ($validationError !== null) {
Expand Down Expand Up @@ -288,6 +317,20 @@ public function update(string $uuid): JSONResponse
return $authError;
}

// C2: Gate writes on ac.bijwerken scope.
if ($this->zgwService->consumerHasScope($this->request, 'ac', 'ac.bijwerken') === false) {
return new JSONResponse(
data: [
'type' => 'PermissionDenied',
'code' => 'permission_denied',
'title' => 'U heeft geen toestemming om autorisaties bij te werken.',
'status' => Http::STATUS_FORBIDDEN,
'detail' => 'Scope ac.bijwerken is vereist.',
],
statusCode: Http::STATUS_FORBIDDEN
);
}

if ($this->zgwService->getConsumerMapper() === null) {
return new JSONResponse(
data: ['detail' => 'Consumer service not available'],
Expand Down Expand Up @@ -369,6 +412,20 @@ public function destroy(string $uuid): JSONResponse
return $authError;
}

// C2: Gate writes on ac.verwijderen scope.
if ($this->zgwService->consumerHasScope($this->request, 'ac', 'ac.verwijderen') === false) {
return new JSONResponse(
data: [
'type' => 'PermissionDenied',
'code' => 'permission_denied',
'title' => 'U heeft geen toestemming om autorisaties te verwijderen.',
'status' => Http::STATUS_FORBIDDEN,
'detail' => 'Scope ac.verwijderen is vereist.',
],
statusCode: Http::STATUS_FORBIDDEN
);
}

if ($this->zgwService->getConsumerMapper() === null) {
return new JSONResponse(
data: ['detail' => 'Consumer service not available'],
Expand Down
3 changes: 1 addition & 2 deletions lib/Controller/BrcController.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@

use OCA\Procest\Service\SettingsService;
use OCA\Procest\Service\ZgwService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
Expand All @@ -56,7 +55,7 @@
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
class BrcController extends Controller
class BrcController extends ZgwController
{
/**
* The ZGW API identifier for the Besluiten register.
Expand Down
3 changes: 1 addition & 2 deletions lib/Controller/DrcController.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
namespace OCA\Procest\Controller;

use OCA\Procest\Service\ZgwService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataDownloadResponse;
use OCP\AppFramework\Http\JSONResponse;
Expand All @@ -54,7 +53,7 @@
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
class DrcController extends Controller
class DrcController extends ZgwController
{
/**
* The ZGW API identifier for the Documenten register.
Expand Down
3 changes: 1 addition & 2 deletions lib/Controller/NrcController.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
namespace OCA\Procest\Controller;

use OCA\Procest\Service\ZgwService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
Expand All @@ -45,7 +44,7 @@
*
* @SuppressWarnings(PHPMD.TooManyPublicMethods)
*/
class NrcController extends Controller
class NrcController extends ZgwController
{
/**
* The ZGW API identifier for the Notificaties register.
Expand Down
41 changes: 41 additions & 0 deletions lib/Controller/ZgwController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

/**
* Procest ZGW Base Controller
*
* Abstract base class for all ZGW API controllers. Serves as the identity
* marker used by ZgwAuthMiddleware to decide which controllers require JWT
* validation and scope enforcement.
*
* @category Controller
* @package OCA\Procest\Controller
*
* @author Conduction Development Team <info@conduction.nl>
* @copyright 2024 Conduction B.V.
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* SPDX-License-Identifier: EUPL-1.2
* SPDX-FileCopyrightText: 2024 Conduction B.V. <info@conduction.nl>
*
* @version GIT: <git-id>
*
* @link https://procest.nl
*/

declare(strict_types=1);

namespace OCA\Procest\Controller;

use OCP\AppFramework\Controller;

/**
* Abstract base class for all ZGW API controllers.
*
* ZgwAuthMiddleware uses `instanceof ZgwController` to identify which
* controllers fall under ZGW JWT authentication and scope enforcement.
* Any controller that handles a ZGW API endpoint must extend this class
* so the middleware's guard is actually exercised.
*/
abstract class ZgwController extends Controller
{
}//end class
3 changes: 1 addition & 2 deletions lib/Controller/ZrcController.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
use DateInterval;
use DateTime;
use OCA\Procest\Service\ZgwService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IL10N;
Expand All @@ -58,7 +57,7 @@
* @SuppressWarnings(PHPMD.TooManyMethods)
* @SuppressWarnings(PHPMD.TooManyPublicMethods)
*/
class ZrcController extends Controller
class ZrcController extends ZgwController
{
/**
* The ZGW API group for this controller.
Expand Down
3 changes: 1 addition & 2 deletions lib/Controller/ZtcController.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
namespace OCA\Procest\Controller;

use OCA\Procest\Service\ZgwService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
Expand All @@ -51,7 +50,7 @@
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
class ZtcController extends Controller
class ZtcController extends ZgwController
{
/**
* The ZGW API identifier for the Catalogi register.
Expand Down
3 changes: 2 additions & 1 deletion lib/Middleware/ZgwAuthMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,15 @@ public function beforeController($controller, $methodName): void
}

// Validate JWT signature via OpenRegister's AuthorizationService.
// M3: Log detailed message server-side; surface only a generic message to caller.
try {
$this->authorizationService->authorizeJwt(authorization: $authorization);
} catch (\Exception $e) {
$this->logger->warning(
'ZGW auth failed: '.$e->getMessage()
);
throw new ZgwAuthException(
message: $e->getMessage(),
message: 'Authenticatiegegevens zijn niet geldig.',
statusCode: Http::STATUS_FORBIDDEN
);
}
Expand Down
34 changes: 25 additions & 9 deletions lib/Service/ZgwService.php
Original file line number Diff line number Diff line change
Expand Up @@ -622,13 +622,20 @@ public function validateJwtAuth(IRequest $request): ?JSONResponse
authorization: $authHeader
);
} catch (\Throwable $e) {
// M3: Log detail server-side but never surface internal JWT validation
// messages in the HTTP response — they aid algorithm/issuer enumeration.
$this->logger->warning(
'ZGW JWT validation failed: '.$e->getMessage(),
['app' => 'procest']
);

return new JSONResponse(
data: [
'type' => 'NotAuthenticated',
'code' => 'not_authenticated',
'title' => 'Authenticatiegegevens zijn niet geldig.',
'status' => 403,
'detail' => $e->getMessage(),
'detail' => 'Authenticatiegegevens zijn niet geldig.',
],
statusCode: Http::STATUS_FORBIDDEN
);
Expand All @@ -653,29 +660,34 @@ public function validateJwtAuth(IRequest $request): ?JSONResponse
*/
public function consumerHasScope(IRequest $request, string $component, string $scope): bool
{
// H2: Fail closed — if ConsumerMapper is not available we cannot verify scope,
// so we must deny rather than grant.
if ($this->consumerMapper === null) {
return true;
return false;
}

try {
$authHeader = $request->getHeader('Authorization');
$token = str_replace('Bearer ', '', $authHeader);
$parts = explode('.', $token);
// H2: Malformed JWT → deny, not grant.
if (count($parts) !== 3) {
return true;
return false;
}

$payload = json_decode(base64_decode($parts[1]), true);
$clientId = $payload['client_id'] ?? ($payload['iss'] ?? null);
// H2: Missing client_id/iss → deny, not grant.
if ($clientId === null) {
return true;
return false;
}

$consumers = $this->consumerMapper->findAll(
filters: ['name' => $clientId]
);
// H2: No matching consumer → deny, not grant.
if (empty($consumers) === true) {
return true;
return false;
}

$consumer = $consumers[0];
Expand Down Expand Up @@ -726,29 +738,33 @@ public function consumerHasScope(IRequest $request, string $component, string $s
*/
public function getConsumerAuthorisaties(IRequest $request, string $component): ?array
{
// H2: Fail closed — if ConsumerMapper unavailable, return empty set (not null/unrestricted).
if ($this->consumerMapper === null) {
return null;
return [];
}

try {
$authHeader = $request->getHeader('Authorization');
$token = str_replace('Bearer ', '', $authHeader);
$parts = explode('.', $token);
// H2: Malformed JWT → return empty (restricted), not null (unrestricted).
if (count($parts) !== 3) {
return null;
return [];
}

$payload = json_decode(base64_decode($parts[1]), true);
$clientId = $payload['client_id'] ?? ($payload['iss'] ?? null);
// H2: Missing client_id/iss → return empty (restricted).
if ($clientId === null) {
return null;
return [];
}

$consumers = $this->consumerMapper->findAll(
filters: ['name' => $clientId]
);
// H2: No matching consumer → return empty (restricted).
if (empty($consumers) === true) {
return null;
return [];
}

$consumer = $consumers[0];
Expand Down
Loading