Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

Release: merge development into beta#71

Open
github-actions[bot] wants to merge 290 commits into
betafrom
development
Open

Release: merge development into beta#71
github-actions[bot] wants to merge 290 commits into
betafrom
development

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Automated PR to sync development changes to beta for beta release.

Merging this PR will trigger the beta release workflow.

Reminder: Add a major, minor, or patch label to this PR to control the version bump. Default is patch.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/docudesk
Commit 1a6f970
Branch 71/merge
Event pull_request
Generated 2026-03-19 19:06 UTC
Workflow Run https://github.com/ConductionNL/docudesk/actions/runs/23312024194

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (110 total)

Metric Count
Approved (allowlist) 110
Approved (override) 0
Denied 0

npm dependencies (266 total)

Metric Count
Approved (allowlist) 265
Approved (override) 1
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 10 statements)

Integration Tests (Newman)

Newman integration tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/docudesk
Commit cc14e0c
Branch 71/merge
Event pull_request
Generated 2026-03-19 21:38 UTC
Workflow Run https://github.com/ConductionNL/docudesk/actions/runs/23318043060

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (110 total)

Metric Count
Approved (allowlist) 110
Approved (override) 0
Denied 0

npm dependencies (266 total)

Metric Count
Approved (allowlist) 265
Approved (override) 1
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 10 statements)

Integration Tests (Newman)

Newman integration tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

Backend:
- TemplateVersionService: version history with create, list, restore, diff
- TemplatePreviewService: render templates with sample data
- TemplateRenderer: conditional section conversion (data-attributes to Twig)
- TemplateService: versioning on update, duplication, optimistic locking
- OpenRegisterResolver: version schema resolution
- TemplatesController: 8 new endpoints (versions, preview, duplicate, lock)
- Register JSON: templateVersion schema, category/tags/lock fields on template

Frontend:
- Template store (Pinia) with full CRUD, versioning, preview, lock actions
- TemplateIndex view with list table
- TemplateDetail view placeholder
- Navigation: Templates menu item
- Views.vue: template view integration

Tests:
- TemplateRendererTest: conditional section conversion with all operators
- SigningRequestList, SigningRequestDetail, SigningRequestForm
- BulkSigningPanel, SignatureVerification
- SigningRequestList, SigningRequestDetail, SigningRequestForm
- BulkSigningPanel, SignatureVerification
# Conflicts:
#	openspec/changes/digital-signing-integration/tasks.md
#	openspec/changes/enhanced-anonymization/design.md
#	openspec/changes/enhanced-anonymization/proposal.md
#	openspec/changes/enhanced-anonymization/specs/anonymization-entity-review/spec.md
#	openspec/changes/enhanced-anonymization/specs/anonymization/spec.md
#	openspec/changes/enhanced-anonymization/specs/batch-anonymization/spec.md
#	openspec/changes/enhanced-anonymization/tasks.md
All tasks complete. Archived to
openspec/changes/archive/2026-03-22-advanced-template-management/
Add batch anonymization pipeline for WOO compliance:
- BatchStateService: ICache-based batch state management
- BatchUploadService: multi-file upload handling
- BatchExtractionService: sequential entity extraction
- BatchAnonymizeService: batch document anonymization
- BatchReportService: CSV audit report generation
- EntityConsolidationService: cross-file entity deduplication
- WooProfileService: WOO entity category profiles
- BatchAnonymizationController: all batch API endpoints
- Batch Pinia store and Vue components (BatchAnonymizationView, EntityReviewTable)
- OpenSpec artifacts: proposal, design, specs, and tasks
Add batch anonymization pipeline for WOO compliance:
- BatchStateService: ICache-based batch state management
- BatchUploadService: multi-file upload handling
- BatchExtractionService: sequential entity extraction
- BatchAnonymizeService: batch document anonymization
- BatchReportService: CSV audit report generation
- EntityConsolidationService: cross-file entity deduplication
- WooProfileService: WOO entity category profiles
- BatchAnonymizationController: all batch API endpoints
- Batch Pinia store and Vue components (BatchAnonymizationView, EntityReviewTable)
- OpenSpec artifacts: proposal, design, specs, and tasks
- Register batch API routes in appinfo/routes.php
- Add batchAnonymization to navigation store types
- Export batch store from store.js
- Add BatchAnonymizationView to Views.vue router
- Add Batch Mode button to AnonymizationWidget
- Update openspec config schema
Fixes CRITICAL verification findings:
- SigningRequest/SignerRecord/SigningAuditEntry schemas in register JSON
- Signing register definition added
- 9 signing API routes in routes.php
- Signing config keys in SettingsService
- Version bumped to 4.0.0
- Archive change to openspec/changes/archive/2026-03-22-enhanced-anonymization/
- Sync new capability specs: batch-anonymization, anonymization-entity-review
Manually archived due to existing admin-settings spec format
incompatibility (table format vs SHALL/MUST requirement format).
All 36 tasks complete, implementation verified.
chore: add missing generated artifact entries to .gitignore
chore: Update openspec config and gitignore docs artifacts
…hanges

chore: Restructure specs to proper OpenSpec format
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/docudesk
Commit 7531e25
Branch 71/merge
Event pull_request
Generated 2026-03-22 21:13 UTC
Workflow Run https://github.com/ConductionNL/docudesk/actions/runs/23412628554

Summary

Group Result
PHP Quality FAIL
Vue Quality FAIL
Security FAIL
License FAIL
PHPUnit FAIL
Newman FAIL

PHP Quality

Tool Result
lint FAIL
phpcs FAIL
phpmd FAIL
psalm FAIL
phpstan FAIL
phpmetrics FAIL

Vue Quality

Tool Result
eslint FAIL
stylelint FAIL

Security

Ecosystem Result
composer FAIL
npm FAIL

License Compliance

Ecosystem Result
composer FAIL
npm FAIL

PHPUnit Tests

PHP Nextcloud Result
Overall FAIL

Integration Tests (Newman)

Result
FAIL

Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

- Move 3 proposed specs (document-creatie-sjablonen, document-signing, register-i18n) to changes/
- Add status: implemented frontmatter to woo-transparency stub
- Create .openspec.yaml, proposal.md, tasks.md for each new change
- Rename '## ADDED Requirements' to '## Requirements' in archive specs
chore: Final OpenSpec structure cleanup
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/docudesk
Commit 7f052ed
Branch 71/merge
Event pull_request
Generated 2026-03-22 21:15 UTC
Workflow Run https://github.com/ConductionNL/docudesk/actions/runs/23412642650

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit FAIL
Newman SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (110 total)

Metric Count
Approved (allowlist) 110
Approved (override) 0
Denied 0

npm dependencies (266 total)

Metric Count
Approved (allowlist) 265
Approved (override) 1
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall FAIL

Integration Tests (Newman)

Newman integration tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

chore(docudesk): Convert all specs to change proposals
…templates

ci: regenerate docs/features.json from openspec/specs/ [skip ci]
…ient IP, add publiccode.yml (#276)

- RegisterDiscoveryService: remove ambiguous `use Exception` import and use
  fully-qualified `\Exception` in the catch block so Psalm/PHPStan cannot
  mis-resolve it to the non-existent OCA\DocuDesk\Service\Exception class.
- appinfo/routes.php: remove the duplicate signing-routes block that was
  declared twice (lines 66-74 and 97-105); route names are now unique.
- SigningService: inject OCP\IRequest via constructor and replace the
  direct $_SERVER['REMOTE_ADDR'] read in getClientIp() with
  $this->request->getRemoteAddress() (Nextcloud proxy-aware, testable).
- publiccode.yml: add minimal valid publiccode.yml (v0.4) for Dutch-gov
  compliance — name, repo URL, version, license EUPL-1.2, nl+en descriptions.
- Add @NoCSRFRequired to all SigningController methods (fixes 412 CSRF errors)
- Catch Throwable in listRequests() to handle missing ObjectService methods gracefully
- Add tests/e2e/spec-coverage.spec.ts covering all 14 openspec feature groups:
  admin-settings, dashboard, consent-management, anonymization,
  anonymization-entity-review, batch-anonymization, template-management,
  prometheus-metrics, pdf-generation, print-preview, signing-requests,
  document-register, metadata-enrichment, ocr-document-scanning,
  letter-correspondence-generation
- 44 tests, all passing against localhost:8080
test: add Playwright e2e spec-coverage suite (44 tests, 14/14 specs)
…age"

This reverts commit 6aa5f49, reversing
changes made to 974e86c.
revert: undo #277 — drop @NoCSRFRequired (security) + API-style e2e tests
…g) (#278)

Gate-5 (route-auth): was PASS before commit; confirmed clean.
Gate-6 (orphan-auth): was PASS; confirmed clean.
Gate-7 (no-admin-idor): 35 methods across 8 controllers lacked 401 guard.
  Added userSession->getUser()===null → STATUS_UNAUTHORIZED guard to:
  ConsentController (5), CorrespondenceController (3), MetadataController (1),
  PdfController (2), PrintController (2), SettingsController::create (1),
  SigningController (8), TemplatesController (13).
  IUserSession injected into controllers that lacked it.
Gate-9 (semantic-auth): was PASS; confirmed clean.
Gate-14 (route-reachability): route names for BatchAnonymizationController
  updated from snake_case batch_anonymization# to camelCase batchAnonymization#
  to match the scanner's slug-derivation convention; URLs/verbs unchanged.
phpstan-baseline.neon regenerated (FP pattern delta for TesseractOCR methods);
PHPStan result: [OK] No errors.
Gate-17 deferred per instructions.
Annotates all 336 scenarios across 15 specs with either @e2e exclude
(pure-backend / unbuilt-UI) or @e2e test references, then adds
24 Playwright UI tests for the 31 UI-observable scenarios that span
admin-settings, anonymization, consent-management, and dashboard.

Whole-spec excludes (pure backend / no current UI surface):
  openregister-bridge, pdf-generation, prometheus-metrics,
  metadata-enrichment, document-register,
  letter-correspondence-generation, batch-anonymization,
  anonymization-entity-review, template-management

Per-scenario excludes (backend internals within UI specs):
  admin-settings: auto-init, version gates, REST API details,
    helper methods, compatibility metadata
  anonymization: upload/extract/anonymize backend pipeline,
    entity normalization, UUID, auth checks, service resolution
  consent-management: service internals, RBAC flags, API endpoints,
    deadline calculations, no-creation-endpoint gap
  dashboard: controller internals, CSP, dead-code removal,
    Pinia wiring, widget script loading

Gate-19 final totals: 336 scenarios, 31 covered by UI tests,
305 excluded with reasons, 0 uncovered.
test(gate-19): bring e2e-coverage to 0 uncovered scenarios
…e verify (#291)

Addresses three confirmed critical findings:

#284 Signature verification always returned valid. SigningVerificationService
no longer trusts the self-asserted /DocuDesk-Signature(...) blob: it is now
fail-closed (valid=false) unless a server-held HMAC over the document
content-hash verifies. Unverifiable /Type /Sig entries report valid=false.
Real PAdES/CMS validation remains out of scope and is documented in the code.

#283 Consent decisions were readable/forgeable cross-user. Removed the
_rbac:false/_multitenancy:false bypass on consent get/update/create so
OpenRegister enforces access, added an explicit per-object ownership guard
in ConsentController (admins exempt; non-owner -> 404), removed
@NoCSRFRequired from the state-changing create/update endpoints, and
whitelisted the mutable fields in updateConsentStatus.

#282 Sign-as-anyone. SigningService::sign() and decline() now require the
authenticated user's UID to match the signer record's userId, else 403.

Updated ConsentControllerTest for the new constructor and added ownership
guard coverage.
#285, #286) (#292)

#285 — Numeric PII silently skipped by anonymization (HIGH)
==============================================================
EntityDetectionService::mapEntitiesForAnonymization dropped any
detected entity where strlen($text) < 3 OR is_numeric($text) === true.
That meant BSN, IBAN, phone numbers, postcodes, bank/account numbers
and case-reference numbers — all the most sensitive identifiers — were
detected by the NER pipeline but never forwarded to OpenRegister for
redaction. They survived verbatim in the 'anonymized' output. The
byte-based strlen was also inconsistent for multibyte text.

Fix:
  * Replace the indiscriminate strlen<3 || is_numeric heuristic with a
    type-aware rule:
      - Typed PII (BSN/IBAN/PHONE/POSTCODE/ACCOUNT/CASE_NUMBER/KVK/BTW/
        EMAIL/PERSON/ADDRESS/LOCATION/ORGANIZATION/DATE/CREDIT_CARD/SSN
        — see TYPED_PII_TYPES) is ALWAYS forwarded for redaction,
        regardless of length or numeric-ness.
      - Only UNKNOWN / unclassified types are still subject to a noise
        floor, now measured with mb_strlen (characters, not bytes) and
        lowered to UNTYPED_MIN_LENGTH = 2 so multi-byte single-character
        tokens are correctly counted.
  * The is_numeric() exclusion is removed entirely.

#286 — Anonymization reports a fabricated replacement count (HIGH)
===================================================================
AnonymizationService set
  $resultInfo['replacementCount'] = count($mappedEntities);
i.e. the count of entities SENT to the redaction step, not the count
that was actually found and replaced. When a detected value was
absent from the literal file text (OCR variance, hyphenation,
formatting variants) the count was still reported as success, and a
reviewer relying on it could publish an under-redacted document.

Fix:
  * Capture a textual projection of the ORIGINAL document before
    calling FileService::anonymizeDocument (readNodeTextSafely):
      - text/* / application/json / application/xml / application/x-yaml
        nodes: read content directly.
      - PDF/DOCX/XLSX and other binary formats: return null, so we
        signal that verification is not possible at this layer
        instead of silently lying.
  * verifyReplacements walks the mapped entities and counts how many
    are literally present (mb_stripos, UTF-8, case-insensitive — mirrors
    OR's str_ireplace semantics in DocumentProcessingHandler).
  * The result now surfaces:
      - replacementsAttempted: int            (sent to OR)
      - replacementsApplied:   int|null       (literally present, or
                                               null when format is
                                               unverifiable)
      - replacementsVerified:  bool           (false for PDF/DOCX/…)
      - unmatchedEntities:     array<text,type>  (sent but not present)
  * The legacy replacementCount field is preserved for backwards
    compatibility but now reflects replacementsApplied (the truth)
    when verifiable, and falls back to replacementsAttempted with
    replacementsVerified=false otherwise.
  * A WARNING-level log is emitted whenever sent != applied so the
    discrepancy is visible in ops dashboards.

Tests
=====
  * EntityDetectionServiceTest:
      - testMapEntitiesForAnonymizationKeepsNumericBsnPii — asserts the
        bug-case from #285: BSN '123456782' is now forwarded.
      - testMapEntitiesForAnonymizationKeepsShortAndNumericTypedPii —
        phone '06' / '0612345678', postcode '1234AB', IBAN.
      - testMapEntitiesForAnonymizationDropsUntypedSingleCharNoise —
        UNKNOWN single-char tokens still dropped (no over-correction).
      - testMapEntitiesForAnonymizationUsesCharacterLengthForUntypedTokens
        — mb_strlen vs strlen multibyte difference.
      - The pre-existing 'FiltersShortAndNumeric' test was rewritten to
        assert the new correct behaviour (typed PII kept, noise
        dropped) instead of the old buggy behaviour.
  * AnonymizationServiceTest:
      - testReplacementCountIsNoLongerFabricatedFromMappedEntities —
        asserts the exact #286 bug-line is gone.
      - testVerificationHelpersExist.
      - testVerifyReplacementsDistinguishesAppliedFromUnmatched —
        4 sent, 3 in source → applied=3, unmatched=[Jo-hn].
      - testVerifyReplacementsIsCaseInsensitive — matches OR's
        str_ireplace.
      - testVerifyReplacementsReportsUnverifiedForBinaryFormats —
        null source → applied=null, verified=false.
      - testReadNodeTextSafelyReturnsNullForBinaryMime —
        application/pdf → null.
      - testReadNodeTextSafelyReturnsContentForTextMime — text/plain
        → content returned.

Verification on live Nextcloud (localhost:8080, container deploy)
================================================================
BEFORE FIX (#285 reproduction): of 4 sent entities (BSN, phone,
postcode, person), only 2 forwarded — BSN and phone silently dropped.
AFTER FIX: all 4 forwarded.

#286 fixture (4 sent, source has 3 literally):
  replacementsAttempted: 4
  replacementsApplied:   3
  replacementsVerified:  true
  unmatchedEntities:     [{text: 'Jo-hn', entityType: 'PERSON'}]

Binary degradation (null source):
  replacementsApplied:   null
  replacementsVerified:  false

phpcs (--standard=phpcs.xml — lib/ scope) on the two edited lib files:
  0 errors, 0 warnings (added @SPEC class tags addressed the pre-existing
  SpecTagSniff warnings on both services).

phpunit (anonymization suite): 27 tests, 73 assertions, all passing.
…udit immutability (#287, #288, #289) (#293)

Three MEDIUM-severity findings on the signing pipeline from the
2026-05-27 team-reviewer pass, fixed together because they all touch
the same end-to-end native-signing flow and the audit register that
backs it.

#287 — NativeSigningProvider sessions are no longer per-request only
  The provider held sessions in a per-instance `$sessions` array, so
  `initiateSigning()` created a row that `checkStatus()`,
  `downloadSignedDocument()` and `cancelSigning()` (running in fresh
  HTTP requests with a freshly-empty array) could never see — every
  native flow failed with "session not found". A new `signingSession`
  schema is added to the `signing` register; sessions are persisted via
  the canonical OR `ObjectService::saveObject(...)` /
  `findAll(['filters' => ['externalId' => ...]])` surface, keyed by
  `externalId`. The SES marker / HMAC embedding in
  `downloadSignedDocument()` is acknowledged as a separate follow-up:
  until the PDF marker writer ships, the method returns the persisted
  `signedDocumentPath` (or falls back to the original `documentPath`)
  and logs an info-level note flagging that the marker hasn't been
  embedded yet. The signing register's version is bumped 1.0.0 → 1.1.0
  and the top-level register schema 5.0.0 → 5.1.0 to trigger OR's
  `imported_config_docudesk_version` gate on upgrade.

#288 — SigningController::listRequests() no longer masks real failures
  The broad `catch (\Throwable $e)` previously returned an empty list
  with `notConfigured: true` and logged at WARNING for any failure — an
  OR/DB outage was indistinguishable from "register not configured yet".
  The catch is narrowed to `\Error` (the genuine missing-OR-method /
  sidecar-lag case the original comment claimed to handle) and logs at
  ERROR; real `\Exception` / runtime infra failures now propagate to
  the framework's 500 handler so monitoring sees them. The
  `RegisterNotConfiguredException` branch is retained for the calm
  "setup state" empty-state UI.

#289 — Signing audit immutability is enforced at the storage layer
  `SigningAuditService::rejectUpdate()` / `rejectDelete()` were
  misleading dead code: nothing routed OR mutations through them. They
  were removed. The `signingAuditEntry` schema in
  `lib/Settings/docudesk_register.json` carries `immutable: true` AND
  `appendOnly: true`, so OR enforces append-only at the mapper level
  for every code path — the real Archiefwet-1995 guarantee, no longer
  contingent on an in-app routing pattern that didn't exist.

Tests:
  - tests/unit/Service/Signing/NativeSigningProviderTest.php (6 tests)
  - tests/unit/Controller/SigningControllerTest.php (4 tests)
  - tests/unit/Service/SigningAuditServiceTest.php (5 tests)
  All 15 new tests pass (34 assertions). phpcs clean on touched files
  (only pre-existing class-level @SPEC warnings remain).
…#290) (#294)

(a) SigningAuditService::getAuditTrail - replace full-register getObjects
    load + PHP array_filter with a server-side searchObjects call filtered
    on signingRequestId; prevents excessive memory use as audit log grows
    (finding #290a).

(b) ConsentCrudService::createFromRequest - drop the merge-all-extra-fields
    approach; only documentId/entityType/entityText are forwarded to
    createConsentRequest; all other request params (consentStatus,
    publicationDecision, userId, etc.) are silently dropped so callers
    cannot forge initial consent state (finding #290b).

Also: add @SPEC class-level tag to both classes (pre-existing PHPCS warning);
update OpenRegisterStubs searchObjects() to accept a query array param;
add unit tests asserting the server-side filter (searchObjects called with
signingRequestId) and the extra-fields drop (createConsentRequest called
without $extra).
PHPCS: phpcbf auto-fixes 5 missing //end-try closing comments in
SettingsController, SigningVerificationService, and TemplatesController.

PHPStan: wire ALLOWED_CREATE_FIELDS constant into createFromRequest() via
array_intersect_key so it is no longer unused; remove redundant ?? '' from
getSigningSecret() where getValueString() already returns a non-nullable string.

PHPMD (easy): rename short vars $mc→$minConfidence, $p→$params, $d→$data;
hoist count() out of for-loop in BatchUploadService; eliminate 3 ElseExpression
violations in AnonymizationController, BatchAnonymizationController, and
AnonymizationService using early-return / default-then-override patterns.
fix: resolve PHPCS, PHPStan, and PHPMD violations
Adds `x-openregister-lifecycle` to the signingRequest schema so OR's
platform-annotations Newman suite can drive the signing workflow via
the standard `/api/objects/{id}/transition` endpoint.

field: status (matches the existing enum on the schema)
initial: DRAFT
transitions:
- send     DRAFT       → PENDING       (notify signers)
- start    PENDING     → IN_PROGRESS   (a signer opens the request)
- complete IN_PROGRESS → COMPLETED     (all signers done; terminal)
- decline  IN_PROGRESS → DECLINED      (terminal)
- expire   PENDING     → EXPIRED       (deadline passed; terminal)
- cancel   PENDING     → CANCELLED     (initiator cancels; terminal)

Matches the Newman flow `send → start → complete` exactly, plus the
three terminal-branches needed for completeness.

Closes the docudesk half of the Tier-3 follow-up from OR's session
report; decidesk half lands separately.
feat(schemas): lifecycle annotation on signingRequest
#297)

Replace all positional saveObject($register, $schema, $data) calls with the
named-arg form saveObject(object:, register:, schema:), replace non-existent
getObject(3-arg) with find(id:, register:, schema:)+jsonSerialize, and replace
non-existent getObjects with findAll(['filters'=>...]). Remove the \Error
fallback in SigningController::listRequests — it existed solely to catch the
missing-getObjects deployment-drift case which no longer applies. Fix
NativeSigningProvider::persistSession to use the same canonical surface.
Add missing IUserSession/Http stubs + findAll stub to the test harness.

Closes findings C1.
C2 (MetadataService): removed `_rbac:false/_multitenancy:false` overrides
from find() and saveObject() calls, restoring per-tenant data isolation.

C3 (AnonymizationController): added IRootFolder file-ownership guard;
extract() and anonymize() now return 404 when the calling user's folder
does not contain the requested fileId (prevents cross-user file IDOR).

H1 (ConsentController/ConsentCrudService/ConsentService/ConsentUpdateHandler):
consent list and byDocument endpoints now filter results to the calling user
unless the caller is an admin, preventing cross-user consent data disclosure.

M2 (SigningController): getAudit() now checks that the caller is the
request initiator or a listed signer before returning the audit trail;
non-admin callers who are neither receive 403 Forbidden.

Tests: all 37 unit tests (AnonymizationControllerTest + ConsentControllerTest
+ SigningControllerTest + MetadataControllerTest + MetadataServiceTest) green.
Stubs: IRootFolder moved to OCP\Files namespace; IGroupManager/IUserSession
stubs added; Http STATUS constants added.
M1 (SigningVerificationService::allSignaturesValid): added count > 0 guard
so that an empty signatures array returns false instead of true. A document
with zero verifiable signatures was previously incorrectly reported as valid.

L1 (SigningVerificationService::stripAssertionMac): replaced str_replace with
preg_replace + preg_quote so the MAC value is treated as a literal string
regardless of any regex metacharacters it may contain.

L2 (SigningAuditService::VALID_ACTIONS): added 'START' to the allowed action
list so signing-session initiation events are not silently dropped by the
logEvent() guard.

Tests: new SigningVerificationServiceTest (5 tests) covers M1 (empty array →
false, non-empty all-valid → true, one-invalid → false) and L1 (plain MAC
stripped, regex-metachar MAC stripped). SigningAuditServiceTest gains one new
test asserting 'START' is in VALID_ACTIONS (L2). All 13 tests green.
Resolves CVE-2026-48805, CVE-2026-48806, CVE-2026-48807, CVE-2026-48808,
and CVE-2026-46636 (sandbox bypass / policy bypass variants, 2026-05-27).
All five affect twig/twig <3.27.0. composer audit is now clean.
fix(security): bump twig/twig to ^3.27.0 (CVE-2026-48805/06/07/08, CVE-2026-46636)
C1: SettingsController::create was accessible to any authenticated user,
allowing overwrite of signing_verification_secret and all other app-config
keys including register/schema pointers. Fix: restrict to admin-only
(groupManager::isAdmin check + remove @NoAdminRequired) and add an explicit
WRITABLE_KEYS allowlist in SettingsService::updateSettings so non-listed
keys (incl. signing_verification_secret) are silently rejected.

C2: ObjectionDeadlineChecker::checkObjectionDeadline called OR ObjectService
find() with _rbac:false + _multitenancy:false, enabling cross-tenant consent
record access. Fix: remove both flags so OR enforces caller tenant + RBAC.

C3: RegisterDiscoveryService::fetchAvailableRegisters called
RegisterService::findAll and SchemaMapper::find with _multitenancy:false,
exposing all tenants' registers and schemas via GET api/settings. Fix: remove
_multitenancy:false overrides so discovery is scoped to the caller's tenant.

C4: Drop @NoCSRFRequired from all mutating (POST/PUT/DELETE) endpoints
across the app — @nextcloud/axios already sends the requesttoken header
in the SPA; this change blocks cross-site forgeries with no user impact.
GET routes retain @NoCSRFRequired. Also fix pre-existing PHPCS violations
in ConsentController (ternary + implicit-bool) and SigningController
(alignment + missing //end try).
…wlist (#302)

testUpdateSettingsPersistsValues was passing 'test_key' which is not in
WRITABLE_KEYS, causing setValueString to be called 0 times (expected once)
and the key to be absent from the result.  Switch to 'signing_provider'
which is in the allowlist.

Also add testUpdateSettingsSilentlyRejectsUnknownKeys to lock in the
wave-3 C1 behaviour: unknown keys must be dropped and must never reach
setValueString.
- SigningVerificationService: remove spaces around concat operator in preg_replace
- ConsentCrudService, ConsentService, ConsentUpdateHandler: add missing @param for $ownerUid; fix @param alignment spaces
- SigningService: convert 6 multi-line ternary expressions to if/else blocks (Inline IF not allowed)
fix(quality): resolve all PHPCS errors — composer check:strict now exits 0
…tee HTTP 400

Move entities/bases input validation ahead of verifyFileAccess() in
AnonymizationController::anonymize(). Previously, the file-access guard
ran first: a non-existent fileId returned 404, masking the validation
entirely, and a valid fileId with malformed bases reached the service layer
where it could throw an unhandled exception (HTTP 500).

After this fix, malformed input (bases is not an array, or contains a
non-string item) always returns HTTP 400 with a clear error message,
regardless of whether the referenced file exists.

Two regression tests added to AnonymizationControllerTest verifying the
new ordering: bases validation returns 400 even when the controller's
root-folder mock reports no matching file.

Fixes the Newman test cases "invalid bases - non-array" and
"invalid bases - non-string item" against POST /api/anonymization/anonymize/{fileId}.
fix(anonymization): validate bases before file-access check to guarantee HTTP 400
…atch IDOR + jobId CSPRNG + signer IDOR + consent multitenancy) (#306)

C1 (structural — issue #304): NativeSigningProvider::initiateSigning() and
downloadSignedDocument() now throw a descriptive RuntimeException referencing
#304 so admins who enable signing_enabled=1 see the gap at once rather than
getting a silent no-op. The full request<->provider wiring is deferred to #304.

C2 (batch IDOR): BatchStateService::getBatch() now compares batch['userId']
against the authenticated user UID; non-owners receive a RuntimeException.
Admins bypass the check. IUserSession + IGroupManager injected; tests updated
and three new ownership tests added.

C3 (jobId CSPRNG + jobStatus IDOR): generateJobId() in CorrespondenceService
replaced mt_rand UUID with random_bytes (RFC-4122 v4, CSPRNG). jobStatus in
CorrespondenceController now compares options.userId against the current user
UID and returns 403 on mismatch.

C4 (signer IDOR): SigningService::sign() and decline() now verify
signer['signingRequestId'] === $requestId before accepting the signer
record, preventing a signer-ID-hijack across signing requests.

C5 (consent _rbac:false): Already fixed in a prior wave on development;
confirmed absent in all 6 cited sites (ConsentService, ConsentUpdateHandler,
ConsentCrudService, MetadataService).
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants