Skip to content

feat(integration-registry): 5 built-in IntegrationProvider implementations (umbrella PR 3/N)#1469

Merged
rubenvdlinde merged 2 commits into
developmentfrom
feature/1307/builtin-providers
May 18, 2026
Merged

feat(integration-registry): 5 built-in IntegrationProvider implementations (umbrella PR 3/N)#1469
rubenvdlinde merged 2 commits into
developmentfrom
feature/1307/builtin-providers

Conversation

@rubenvdlinde
Copy link
Copy Markdown
Contributor

Stacked on #1468. Tasks 12-17 of #1307 — Backend — Built-in Providers.

Task Provider Storage Required app Notes
3.1 FilesProvider magic-column null list() delegates to FileService::getFilesForEntity(); mutation throws (FileController owns the write path)
3.2 NotesProvider link-table null Full CRUD delegation to NoteService
3.3 TasksProvider link-table null Full CRUD delegation to TaskService; composite {calendarId}/{taskUri} entity ids
3.4 TagsProvider link-table null list() via ISystemTagObjectMapper; mutation throws (TagsController owns the write path)
3.5 AuditTrailProvider query-time null Read-only by design (AD-22); mutation inherits NotImplementedException from the abstract base
3.6 Bootstrap wiring New registerBuiltinIntegrationProviders() (DI) + bootBuiltinIntegrationProviders() (addProvider) helpers in Application.php

Net new files

  • lib/Service/Integration/BuiltinProviders/FilesProvider.php
  • lib/Service/Integration/BuiltinProviders/NotesProvider.php
  • lib/Service/Integration/BuiltinProviders/TasksProvider.php
  • lib/Service/Integration/BuiltinProviders/TagsProvider.php
  • lib/Service/Integration/BuiltinProviders/AuditTrailProvider.php
  • tests/Unit/Service/Integration/BuiltinProvidersMetadataTest.php

Modified

  • lib/AppInfo/Application.phpregisterBuiltinIntegrationProviders (DI) + bootBuiltinIntegrationProviders (addProvider) helpers
  • openspec/changes/pluggable-integration-registry/tasks.md + plan.json

Tests

  • 45 tests, 80 assertions — all green inside the openregister-postgres dev container
  • 11 new for the 5 providers; the 34 from PRs 1 & 2 still pass

Spec deviations (flagged inline in tasks.md / plan.json)

  1. FilesProvider + TagsProvider keep write paths at FileController / TagsController. Mutation methods throw NotImplementedException with a pointer to the umbrella's controller refactor (tasks 18-22). Surface change for callers: zero.
  2. AuditTrailProvider mutations inherit the abstract base's defaults — per AD-22 query-time providers MUST throw on create/update/delete.
  3. referenceType: <id> declarations the spec calls for on the frontend registry side are deferred to tasks 25-30 (when each provider gets its JS registry counterpart). The PHP-side providers are complete.

Stacking note

Base is feature/1307/schema-validator-refactor (PR #1468), which is in turn stacked on feature/1307/pluggable-integration-registry (PR #1467). When PRs #1467 and #1468 merge into development, GitHub will retarget this PR to development automatically — no rebase needed.

🤖 Generated with Claude Code

…tions (umbrella PR 3/N)

Third slice of #1307 — completes tasks 12-17 of the umbrella. Stacked
on PR #1468 (schema validator refactor), which is itself stacked on
PR #1467 (foundation).

Tasks completed in this slice (6/69 → cumulative 17/69):

- 3.1 FilesProvider wraps FileService (magic-column).
- 3.2 NotesProvider wraps NoteService (link-table, full CRUD).
- 3.3 TasksProvider wraps TaskService (link-table, CalDAV, full CRUD;
  composite {calendarId}/{taskUri} entity ids).
- 3.4 TagsProvider wraps the NC system tag manager (link-table; read
  via ISystemTagObjectMapper::getTagIdsForObjects).
- 3.5 AuditTrailProvider wraps AuditTrailMapper (query-time, AD-22 —
  read-only by design; mutation methods inherit NotImplementedException
  from AbstractIntegrationProvider).
- 3.6 All five register via addProvider() at Application::boot() time
  through a new bootBuiltinIntegrationProviders() helper. The DI
  bindings live in registerBuiltinIntegrationProviders() so each
  provider's wrapped service is resolved lazily.

Spec deviations flagged inline in tasks.md + plan.json:

- FilesProvider + TagsProvider keep their write paths at the existing
  FileController / TagsController routes for now. Mutation methods
  throw NotImplementedException with a pointer to the umbrella's
  controller refactor (tasks 18-22). Users see the right tab; the
  underlying write API stays where it is until the controller layer
  catches up. Surface change is zero.
- AuditTrailProvider intentionally inherits the abstract base's
  mutation defaults — audit-trail entries are immutable by
  construction; per AD-22 query-time providers MUST throw on
  create/update/delete.
- The referenceType: <id> declarations the spec calls for on the
  frontend registry side are deferred to tasks 25-30 (when each
  provider gets its JS registry counterpart). The PHP-side providers
  are complete.

Net new files:
- lib/Service/Integration/BuiltinProviders/FilesProvider.php
- lib/Service/Integration/BuiltinProviders/NotesProvider.php
- lib/Service/Integration/BuiltinProviders/TasksProvider.php
- lib/Service/Integration/BuiltinProviders/TagsProvider.php
- lib/Service/Integration/BuiltinProviders/AuditTrailProvider.php
- tests/Unit/Service/Integration/BuiltinProvidersMetadataTest.php

Modified:
- lib/AppInfo/Application.php — new registerBuiltinIntegrationProviders
  + bootBuiltinIntegrationProviders helpers
- openspec/changes/pluggable-integration-registry/tasks.md
- openspec/changes/pluggable-integration-registry/plan.json

Unit tests:
- 45 tests, 80 assertions — all green (11 new for the 5 providers + the
  34 from PR 1 & 2)

Refs: #1307
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKER] FilesProvider::list() has no user-scope guard

The provider calls FileService::getFilesForEntity($object) without scoping to the current user. If FileService::getFilesForEntity uses a raw IRootFolder path (or no folder at all) rather than IRootFolder->getUserFolder($uid), it can enumerate files belonging to other users. There is no IUser or uid injected into the provider, no evidence FileService internally restricts to the authenticated user's folder. The provider must inject IUserSession and verify returned nodes are within getUserFolder($uid).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKER] NotesProvider methods pass no user context — notes from any user may leak

NoteService::getNotesForObject($objectId, ...) and the create/update/delete methods receive no user identity. If NoteService uses oc_comments rows keyed only on object_id without filtering by actor_id/owner, any authenticated user who knows an objectId can read or mutate notes written by other users. Inject IUserSession and pass the uid, or have NoteService enforce the filter itself.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKER] TasksProvider::list() fetches tasks with no user/calendar ownership check

TaskService::getTasksForObject($objectId) is called without any uid scoping. CalDAV task lists are per-user; without filter, the provider can return tasks belonging to other users. calendarId in create() (line 918) is caller-supplied with no ownership verification — a user can create tasks in another user's calendar. Validate calendar ownership before delegating.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKER] Bare catch(\Throwable $e){} silently swallows provider construction failures (Gate 7)

The inner loop catches every \Throwable with an empty body and no logging. A misconfigured or maliciously broken provider will fail invisibly, making debugging impossible and masking security-relevant injection errors. At minimum log at warning level with $e->getMessage() and the provider class name. The outer catch at line 123 has the same issue.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKER] AuditTrailProvider::list() has no user-scope filter — exposes all objects' audit trail

AuditTrailMapper::findAllByObject($objectId) returns every audit entry for that object regardless of caller. Audit trails contain sensitive mutation history. The provider must verify the caller has read access to the parent object before surfacing its trail. No IUserSession is injected, so no ownership check is possible in the current implementation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKER] resolveObject() catches \Throwable with silent continue (Gate 7)

Lines 458-460 catch every \Throwable and silently continue — a real infrastructure failure (DB down, OOM) is treated the same as 'mapper not found'. The outer list() catch at line 431 does the same. Log at debug or warning level so the silent-failure gate is satisfied.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CONCERN] Pagination default of 50 hardcoded; _page is zero-based — inconsistent with OR standard

$limit = (int) ($filters['_limit'] ?? 50) uses an undocumented default; offset is _page * limit (zero-based). OR's standard pagination typically uses _offset. Document the convention in the method docblock, or align with the OR-wide filter contract.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CONCERN] TagsProvider hardcodes object type 'openregister' — silent empty for other types

getTagIdsForObjects([$objectId], 'openregister') hardcodes the object type. If OR object IDs were registered under a different type (e.g. fully-qualified class name, schema-scoped), the call returns an empty array and no error. The object type should be a named constant or injected, with a test covering the non-empty path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CONCERN] Test suite only covers metadata + NotImplementedException — no get()/list() delegation tests

The test docblock acknowledges wrapped-service delegation is not covered, deferring to 'integration tests'. But FilesProvider::list(), TasksProvider::list/create/delete, TagsProvider::list each have their own logic (resolveObject loop, splitEntityId, getTagIdsForObjects mapping). These are unit-testable with mocks and would catch the user-scoping bugs above.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CONCERN] TasksProvider::create() accepts caller-supplied calendarId without validation

$calendarId = (string) ($payload['calendarId'] ?? '') passes the value directly to TaskService::createTask(). If TaskService does not verify the calendar belongs to the current user, an authenticated user can append tasks to another user's calendar. Validate calendar ownership (or shared with write access) before the delegate call.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CONCERN] AuditTrailProvider::list() uses method_exists() duck-typing instead of a stable mapper interface

The provider checks method_exists($this->mapper, 'findAllByObject') then falls back to findAll(filters: ...). The production call path depends on which method the injected mapper happens to expose at runtime, making behaviour non-deterministic and untestable. Define a mapper interface and declare it as the constructor parameter type.

Copy link
Copy Markdown
Contributor

@WilcoLouwerse WilcoLouwerse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

🔴 Blockers (6)

🟡 Concerns (5)

🟢 Minor (1)

  • bootBuiltinIntegrationProviders() declares parameter type as untyped (lib/AppInfo/Application.php:117)
    private function bootBuiltinIntegrationProviders($server): void omits the type hint for $server. Type as \OCP\IServerContainer or Psr\Container\ContainerInterface for static analysis.

Reviewed by WilcoLouwerse via automated batch review.

Base automatically changed from feature/1307/schema-validator-refactor to development May 18, 2026 11:19
@rubenvdlinde rubenvdlinde merged commit 5414e3c into development May 18, 2026
16 of 19 checks passed
@rubenvdlinde rubenvdlinde deleted the feature/1307/builtin-providers branch May 18, 2026 11:19
@WilcoLouwerse
Copy link
Copy Markdown
Contributor

🔍 Retrospective audit findings — surfaced from #1515

While reviewing integration rollup PR #1515, three findings trace back to the built-in provider implementations in this PR:

  1. 🔴 NotesProvider::update/delete accept any caller-supplied comment id without ownership check — paired with the controller IDOR (see feat(integration-registry): routes + controllers + OCS capability (umbrella PR 4/N) #1473), any authenticated user can edit/delete arbitrary comments by guessing integer IDs. Inline comment on #1515
  2. 🔴 FilesProvider::list calls mapper->find() with no RBAC and the top-level catch (\Throwable) { return []; } swallows permission denials, infrastructure errors, and missing objects into the same empty-list response. Inline comment on #1515
  3. 🔴 MarkerLookupTrait::findByMarker ships error_log() in production (Hydra gate-2 forbidden-pattern). Replace with $this->logger->warning(...). Inline comment on #1515

The PR body of #1515 surfaced finding 1 + 2 under "#1469 per-provider RBAC concerns" as a design-decision question — confirming they merged unresolved. Worth pinning the trust model (provider enforces vs controller enforces vs underlying NC API) and opening a fixup PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants