Repository navigation
feat(contacts): show a contact person's name in Applications in use, on its page and in the organisation's list - #1292
WilcoLouwerse wants to merge 10 commits into
Conversation
…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>
Quality Report — ConductionNL/stackiq @
|
| 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.
…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>
WilcoLouwerse
left a comment
There was a problem hiding this comment.
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
idsargument, so only the first 100 contact persons could be named — fixed in9aaacca2. - 🔴 gate-16: five changed methods had no
@spectag — fixed in9aaacca2. - 🟡 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.jsturns 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;contactPersonandusagekeep no public read rule (REQ-CMDB-010). - The contact persons are read with the caller's RBAC and multitenancy, applied before the
_idsfilter (MagicSearchHandler::buildFilteredQuery). - The frontend cache is per page load; switching organisation reloads the page, so no names cross tenants.
cellWidgetsexists onCnAppRootin 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.
Quality Report — ConductionNL/stackiq @
|
| 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.
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>
Quality Report — ConductionNL/stackiq @
|
| 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.
… 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>
Quality Report — ConductionNL/stackiq @
|
| 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
left a comment
There was a problem hiding this comment.
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@spectags point at it, and its@e2e excludenames the vitest files that assert it. -
No tests (
7ca89b3,afd1c6f):tests/vitest/contactPersonName.spec.jscovers the name, the three empty states and "no hint before the object loads".tests/vitest/usageRiskSignals.spec.jscoversfetchSettingsrunning before the first object read.contactNames.spec.jscovers 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).
What
The Business owner column of Applications in use showed the contact person's uuid. A
contactPersonin OpenRegister carries only acontactsUid; 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_idsin the query), then looks eachcontactsUidup throughStackiqContactSyncService::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.contactNamecell widget (src/cellWidgets.js, handed toCnAppRootascellWidgets): 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: thebusinessOwnercolumn uses it.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-namesendpoint.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
contactsUidset 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 (contactPersonandusagestill 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
ContactNamesControllerTest6 andStackiqContactSyncServiceTest5 passed; vitestcontactNames.spec.js7 passed;npx jest184 passed;node tests/validate-manifest.jsPASS; hydra gates (local, diff scope) 70 of 70 applicable passed; eslint 0 errors.tests/vitest/usages.spec.jshas 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