Skip to content

feat(contacts): show a contact person's name in Applications in use, on its page and in the organisation's list - #1292

Open
WilcoLouwerse wants to merge 10 commits into
developmentfrom
feat/woo586-usage-owner-names
Open

WilcoLouwerse wants to merge 10 commits into
developmentfrom
feat/woo586-usage-owner-names

Conversation

@WilcoLouwerse

@WilcoLouwerse WilcoLouwerse commented Oct 12, 2026 •

Copy link
Copy Markdown

What

The Business owner column of Applications in use showed the contact person's uuid. A contactPerson in OpenRegister carries only a contactsUid; the name lives in Nextcloud Contacts (the CMDB import puts its owners in the importing admin's "Stackiq CMDB owners" address book, REQ-CMDB-010).

How

  • GET /api/contact-names?ids[]=<uuid> (ContactNamesController::index, #[NoAdminRequired], #[UserRateLimit(limit: 120, period: 60)]): reads the contact persons with the caller's own OpenRegister rights (RBAC and multitenancy at their defaults, ids as _ids in the query), then looks each contactsUid up through StackiqContactSyncService::findOwnContactByUid(): an exact UID match that counts only results from the caller's own and shared address books (never the system principal's books: the local system address book and the books synced from trusted servers). Answers {names: {uuid: name}}; 401 when not signed in, 400 above 100 ids; fails closed (no names) without a configured schema, a CardDAV backend or a readable OpenRegister.
  • contactName cell widget (src/cellWidgets.js, handed to CnAppRoot as cellWidgets): batches the cells of one page into one request, caches per page load, keeps the uuid when the caller has no name.
  • manifest.d/usages.json: the businessOwner column uses it.
  • Contact person page and organisation list (ContactPersonName.vue, src/customComponents.js, src/manifest.json): the contact person page opens with the name, and the organisation page's contact person list has a Name column, both read through the same /api/contact-names endpoint.
  • UsageRiskSignals.vue: waits for the app settings before it reads its objects, so the widget no longer shows an error when the usage page is opened straight from a link; its labels take their own width and no longer run into their values.

Privacy

A name is returned only to someone who can already read both the contact person and the contact in their own or shared address books. A contactsUid set to a local or remote user id names nobody. No public route, nothing stored or cached server-side, nothing added to the OpenRegister objects, so OpenCatalogi, portaliq and anonymous OpenRegister reads are unchanged (contactPerson and usage still have no public read rule). Because the owners are in the importing admin's own address book, other users see the uuid as before.

Checks

PHPUnit ContactNamesControllerTest 6 and StackiqContactSyncServiceTest 5 passed; vitest contactNames.spec.js 7 passed; npx jest 184 passed; node tests/validate-manifest.js PASS; hydra gates (local, diff scope) 70 of 70 applicable passed; eslint 0 errors. tests/vitest/usages.spec.js has 2 failures that are identical on plain development. Review fixes: see the review on this PR. Rig check pending.

Jira: WOO-586 — plan: https://github.com/ConductionNL/readonly-mirror-wilco-claude-plans/tree/dev-mirror/issues/jira/WOO-586/

🤖 Generated with Claude Code

…se list

The business owner column showed the contact person's uuid. A contact
person in OpenRegister carries only a contactsUid; the name is in
Nextcloud Contacts (the CMDB import puts its owners in the importing
admin's "Stackiq CMDB owners" address book).

GET /api/contact-names?ids[]=<uuid> gives the names for a signed-in
user: it reads the contact persons with the caller's own OpenRegister
rights and looks each contactsUid up in the address books Nextcloud
gives the caller. A name is only returned to someone who can already
read both the contact person and the contact. There is no public route
and nothing is stored, so the owner stays unreadable for anonymous
visitors, OpenCatalogi and portaliq.

The new contactName cell widget batches the cells of one page into one
request and keeps the uuid when the caller has no name for it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/stackiq @ e8ff5bb

Check PHP Vue Security License Tests
lint ✅
phpcs ✅
phpmd ✅
psalm ✅
phpstan ✅
phpmetrics ✅
eslint ✅
stylelint ✅
build ✅
check-manifest ✅
check-vue-demi ✅
test-l10n ✅
format ✅
check-schema-l10n ✅
check-l10n-js ✅
composer ✅ ✅ 133/133
npm ✅ ✅ 807/807
app:check-code ⏭️
info.xml ✅
REUSE ❌
lockfile sync ✅
PHPUnit ✅ 1229/1229 · 20 skipped
Newman ⏭️
Playwright ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it locally with npx playwright test, or from the Actions tab on a branch with no open pull request into development.
Hydra gates ❌

Quality workflow — 2026-10-12 12:43 UTC

Download the full PDF report from the workflow artifacts.

WilcoLouwerse and others added 2 commits October 12, 2026 15:43
…s, and by the requested ids

Review of #1292:

- The name lookup used IManager::search with its defaults, which also
  searches the system address book (one card per local user, UID = user
  id). A contactsUid is free text on a contactPerson, so whoever may
  write one could have the endpoint name any local user. The new
  StackiqContactSyncService::findOwnContactByUid() passes enumeration
  and fullmatch false (the contacts manager then skips the system
  address book) and strict_search (exact UID, no substring scan).
- OpenRegister's single register+schema search path ignores the ids
  argument and reads `_ids` from the query, so only the first 100
  readable contact persons could ever be named. The ids now go in the
  query as `_ids`.
- #[UserRateLimit(limit: 120, period: 60)] on the endpoint.
- A failed request no longer pins "no name" for the page: its ids are
  dropped from the cache, so the next cell asks again.
- @SPEC tags on the five frontend methods gate 16 named, and a
  scenario under REQ-UAP-003 for who sees a name.

PHPUnit ContactNamesControllerTest 6 and StackiqContactSyncServiceTest
5 pass, vitest contactNames.spec.js 7 pass; removing the `_ids` key
or the cache.delete turns a test red.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s books

Pre-push check of the previous commit: with enumeration and fullmatch
off the contacts manager skips the local system address book, but it
still searches the other books of the system principal, which include
the books synced from trusted servers (one card per remote user, UID =
user id). A contactsUid set to a remote user id could still name that
user.

findOwnContactByUid() now keeps a result only when its addressbook-key
is one of the books the CardDAV backend lists for the user's principal
(own and shared), and finds nothing without a backend or a signed-in
user. The controller docblock and the REQ-UAP-003 scenario say "local
or remote user id".

StackiqContactSyncServiceTest 5 and ContactNamesControllerTest 6 pass;
dropping the address-book check turns the service test red.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread lib/Controller/ContactNamesController.php
Comment thread lib/Controller/ContactNamesController.php
Comment thread src/utils/contactNames.js

@WilcoLouwerse WilcoLouwerse left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Verdict: APPROVE (self-review, posted as a comment)

Mode Strict (new #[NoAdminRequired] read endpoint that returns names of people). The review found 3 🔴, 2 🟡 and 1 🟢, and a pre-push adversarial check of the fix found one more (🟡, federated address books); all were fixed on the branch before posting, and the re-check of the final fix came back clean.

✅ Fixed before posting (7)

  • 🔴 A crafted contactsUid named any local or federated user — fixed in 9aaacca2 + 36a09596.
  • 🔴 OpenRegister ignored the ids argument, so only the first 100 contact persons could be named — fixed in 9aaacca2.
  • 🔴 gate-16: five changed methods had no @spec tag — fixed in 9aaacca2.
  • 🟡 A failed request pinned "no name" for the page life; the ids of a failed chunk are now dropped from the cache, so the next cell asks again (contactNames.spec.js turns red without it) — 9aaacca2.
  • 🟡 Up to 100 substring scans over every address book per request, with no rate limit: the lookup is now an exact UID match, and the endpoint carries #[UserRateLimit(limit: 120, period: 60)] — 9aaacca2.
  • 🟡 (pre-push check) The books synced from trusted servers sit under the system principal but are not flagged as the system address book, so they were still searched; a result now counts only from the caller's own and shared books — 36a09596.
  • 🟢 No spec scenario for the endpoint: REQ-UAP-003 has a scenario for who sees a name — 9aaacca2.

✅ Verified clean

  • Anonymous callers: no #[PublicPage], 401 in the body as well; contactPerson and usage keep no public read rule (REQ-CMDB-010).
  • The contact persons are read with the caller's RBAC and multitenancy, applied before the _ids filter (MagicSearchHandler::buildFilteredQuery).
  • The frontend cache is per page load; switching organisation reloads the page, so no names cross tenants.
  • cellWidgets exists on CnAppRoot in both 2.65.0 and 2.73.1.

Gates: local run on 36a09596 (run-hydra-gates.sh --scope-to-diff --base origin/development): exit 0, 70 of 70 applicable gates ran.
Tests: PHPUnit ContactNamesControllerTest 6 and StackiqContactSyncServiceTest 5 pass; vitest contactNames.spec.js 7 pass; removing the _ids key, the address-book check or the cache drop each turns a test red.
Not verified: in a browser — the WOO-586 rig was stopped during the review.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/stackiq @ 2097548

Check PHP Vue Security License Tests
lint ✅
phpcs ✅
phpmd ✅
psalm ✅
phpstan ✅
phpmetrics ✅
eslint ✅
stylelint ✅
build ✅
check-manifest ✅
check-vue-demi ✅
test-l10n ✅
format ✅
check-schema-l10n ✅
check-l10n-js ✅
composer ✅ ✅ 133/133
npm ✅ ✅ 807/807
app:check-code ⏭️
info.xml ✅
REUSE ❌
lockfile sync ✅
PHPUnit ✅ 1230/1230 · 20 skipped
Newman ⏭️
Playwright ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it locally with npx playwright test, or from the Actions tab on a branch with no open pull request into development.
Hydra gates ✅

Quality workflow — 2026-10-12 14:54 UTC

Download the full PDF report from the workflow artifacts.

WilcoLouwerse and others added 3 commits October 13, 2026 18:31
Opened straight from a link, the usage page can mount the risk signals
before the app has read its settings. No object type resolved then, the
first read threw and the widget showed its error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rganisation's list

A contact person holds only a contactsUid; its name lives in Nextcloud
Contacts. The contact person page now opens with the name, and the
organisation page's contact person list has a Name column, both read
through /api/contact-names, so only someone who can read the contact
sees it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A page-wide style gives every dt a fixed width of 130px, aligned to the
end, so "Known vulnerabilities" and "End of support" ran into their
values. The labels now take their own width and start-align.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/stackiq @ f57c23d

Check PHP Vue Security License Tests
lint ✅
phpcs ✅
phpmd ✅
psalm ✅
phpstan ✅
phpmetrics ✅
eslint ✅
stylelint ✅
build ✅
check-manifest ✅
check-vue-demi ✅
test-l10n ✅
format ✅
check-schema-l10n ✅
check-l10n-js ✅
composer ✅ ✅ 133/133
npm ✅ ✅ 807/807
app:check-code ⏭️
info.xml ✅
REUSE ❌
lockfile sync ✅
PHPUnit ✅ 1230/1230 · 20 skipped
Newman ⏭️
Playwright ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it locally with npx playwright test, or from the Actions tab on a branch with no open pull request into development.
Hydra gates ✅

Quality workflow — 2026-10-13 16:54 UTC

Download the full PDF report from the workflow artifacts.

@WilcoLouwerse WilcoLouwerse changed the title feat(usage): show the business owner by name in the Applications in use list feat(contacts): show a contact person's name in Applications in use, on its page and in the organisation's list Oct 13, 2026
WilcoLouwerse and others added 4 commits October 13, 2026 20:42
… REQ-UAP-005

Closes review finding f2: the two new name surfaces had no scenario and
their @SPEC tags pointed at REQ-UAP-003 (usage owners). REQ-UAP-005 states
who sees the name and the empty states; ContactNameCell is retagged.
Class sweep: every @SPEC tag of ContactPersonName, contactNames.js (new
export) and ContactNameCell now targets REQ-UAP-005 or REQ-UAP-003 by
surface.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes review finding f1: the page showed 'in an address book you cannot
read' for every empty name. contactNameResult() now tells a failed request
from an answer without a name, and the page gets contactsUid from the
object, so it says: not linked, could not be loaded (reload), or the
address-book sentence only after a successful empty answer.
Class sweep: ContactNameCell shows no hint text (it keeps the uuid), so it
needs no change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gnals settings wait

Closes review finding f3: ContactPersonName (name, not linked, no readable
address book, failed request), contactNameResult, and UsageRiskSignals
reading the settings before its first object read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CnDetailPage passes no object while it loads and CnBodySections drops the
unresolved @object.contactsUid, so the 'not linked' hint flashed before the
object arrived. The page now also takes @object.id (loadedId) and shows the
spinner until it is there. Also names the right test files in the REQ-UAP-005
scenario exclude reasons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/stackiq @ c36f8de

Check PHP Vue Security License Tests
lint ✅
phpcs ✅
phpmd ✅
psalm ✅
phpstan ✅
phpmetrics ✅
eslint ✅
stylelint ✅
build ✅
check-manifest ✅
check-vue-demi ✅
test-l10n ✅
format ✅
check-schema-l10n ✅
check-l10n-js ✅
composer ✅ ✅ 133/133
npm ✅ ✅ 807/807
app:check-code ⏭️
info.xml ✅
REUSE ❌
lockfile sync ✅
PHPUnit ✅ 1230/1230 · 20 skipped
Newman ⏭️
Playwright ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it locally with npx playwright test, or from the Actions tab on a branch with no open pull request into development.
Hydra gates ✅

Quality workflow — 2026-10-13 19:10 UTC

Download the full PDF report from the workflow artifacts.

@WilcoLouwerse WilcoLouwerse left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Verdict: APPROVE (Strict) — self-review posted as COMMENT (GitHub blocks self-APPROVE)

Nothing left to address: the 2 concerns and 1 minor found in the 3 new commits, and the concern and minor the pre-push check found in their fix, were fixed on the branch before posting. All 3 round-1 threads stay resolved. What still stands between this PR and a merge: 1 approval.

Round Found Status
1 (36a0959) 3 🔴 · 3 🟡 · 1 🟢, fixed before posting resolved
2 (afd1c6f) — this review 2 🟡 (one hint for every empty name; new name surfaces without a spec scenario) · 1 🟢 (no tests) fixed before posting in 4fd9194, e49e441, 7ca89b3, afd1c6f
Finding Was Now Where
🟡 Hint claims an unreadable address book for any empty name new fixed before posting in e49e441 + afd1c6f src/components/organisations/ContactPersonName.vue
🟡 New name surfaces lack a spec scenario new fixed before posting in 4fd9194 + afd1c6f openspec/specs/application-usage-pages/spec.md (REQ-UAP-005)
🟢 No tests for ContactPersonName and the settings wait new fixed before posting in 7ca89b3 + afd1c6f tests/vitest/
Details of this round

Scope decision: delta 36a0959…cb48f55 (3 commits, 8 files, 154 changed lines: 756d4d9, 2ed63b6, cb48f55), plus this run's fix commits.

✅ Fixed before posting (3)

  • One hint for every empty name (e49e441, afd1c6f). The contact person's name block now separates three cases:

    Case Hint
    The name request failed (429, network, 5xx) "The name could not be loaded. Reload the page to try again."
    No contactsUid "This contact person is not linked to a Nextcloud contact."
    The endpoint answered without a name The address-book sentence

    contactNameResult() tells a failed request from an empty answer. Until the object has loaded (loadedId, @object.id), the block shows the spinner and no hint.

  • No spec scenario (4fd9194, afd1c6f): REQ-UAP-005 "A contact person's name shows only to who can read the contact" covers the page and the list column (who sees the name) and the empty states. The @spec tags point at it, and its @e2e exclude names the vitest files that assert it.

  • No tests (7ca89b3, afd1c6f): tests/vitest/contactPersonName.spec.js covers the name, the three empty states and "no hint before the object loads". tests/vitest/usageRiskSignals.spec.js covers fetchSettings running before the first object read. contactNames.spec.js covers failed versus empty.

✅ Verified clean

  • Both new surfaces use the round-1 endpoint GET /api/contact-names: the contact person page and the organisation's Name column. That endpoint is #[NoAdminRequired], rate-limited, and reads only the caller's own and shared address books by exact UID, under the caller's RBAC.
  • There is no second resolution path, nothing is rendered as v-html, and names are not logged or cached server-side.
  • The endpoint answers the same for "not permitted" and "no name", so nothing leaks.

Not verified: that every OpenRegister object reaches the page with a top-level id. If one does not, the spinner stays.
Gates: CI quality / Hydra Gates ✅ on cb48f55. CI on afd1c6f started with this push.
Checks after the last edit: eslint and prettier clean; the three vitest specs pass (15 tests); test:l10n and check:l10n-js OK. The full vitest suite has 7 failures that also fail on cb48f55 (connectionsPages, maintenance, manifestIncludeKeys, settingsInfoPanels, usages).

This branch has not been deployed

No deployments
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.

1 participant