toga-ai 1.0.772 → 1.0.773
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/1.0/apps/tools/INDEX.md +1 -0
- package/knowledge/1.0/apps/tools/architecture.md +12 -3
- package/knowledge/1.0/apps/tools/features/compass-user-persona-admin.md +138 -0
- package/knowledge/1.0/apps/tools/features/mvc-data-access-patterns.md +36 -2
- package/knowledge/1.0/apps/tools/features/saml-sso-auth.md +19 -2
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/features/effective-persona-resolution.md +88 -0
- package/knowledge/2.0/apps/_underscore/features/record-change-audit-log.md +18 -3
- package/knowledge/2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md +14 -5
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/compass-canada/profile.md +3 -2
- package/knowledge/clients/compass-usa/features/people-file-user-lifecycle.md +35 -5
- package/knowledge/clients/compass-usa/features/persona-model-and-levy-gating.md +16 -2
- package/knowledge/clients/compass-usa/profile.md +3 -2
- package/knowledge/clients/compass-usa/workflows/granting-persona-bundle-access.md +15 -2
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
| [/clickup/aliases — ClickUp Label-Alias Admin (Tools → 2.0 Team schema)](features/clickup-label-aliases-admin.md) | The human side of the NetSuite→ClickUp **Stakeholders / End Customer** labels. | tools/mvc/clickup/aliases/get.php, tools/mvc/clickup/aliases/post.php, tools/_/app/clickup/aliases.php, tools/_/app/nav.php |
|
|
7
7
|
| [Developer Dashboard (ClickUp Sprint, tools /developer)](features/clickup-sprint-dashboard.md) | A native **ClickUp sprint dashboard** in the 1.0 `tools` app at route `/developer` (the **"Developer Dashboard"**, renamed from `/clickup/react` / "Clickup" — s | tools/_/app/clickup/sprint.php, tools/v2/sprints/tile/index.php, tools/v2/sprints/current/index.php, tools/v2/sprints/status-breakdown/index.php, tools/v2/sprints/worktype-breakdown/index.php, tools/v2/sprints/points-by-dev/index.php, tools/v2/sprints/burndown/index.php, tools/assets/clickup/sprint-dashboard.html, tools/mvc/developer/get.php, tools/mvc/login/get.php, tools/_/app/nav.php |
|
|
8
8
|
| [CloudFront Client Setup](features/cloudfront-client-setup.md) | An SSO-gated admin tool at **`/devops/cloudfront-clients`** in the Tools 1.0 app that onboards a client onto **CloudFront + Route 53 across multiple AWS account | tools/_/app/devops/cloudfront.php, tools/mvc/devops/cloudfront-clients/get.php, tools/mvc/devops/cloudfront-clients/post.php, tools/assets/js/cloudfront-clients.js, tools/assets/css/cloudfront-clients.css, tools/_/app/nav.php, tools/_/app/frameworkindex.php, tools/config.production.ini |
|
|
9
|
+
| [Compass User & Persona Admin (tools /compass/users, /compass/personas)](features/compass-user-persona-admin.md) | Two pages in the Tools `developers` nav folder that let staff do, in a browser, the two Compass requests that used to mean "email a developer, who writes a `dbc | tools/_/app/compass.php, tools/mvc/compass/users/get.php, tools/mvc/compass/users/post.php, tools/mvc/compass/personas/get.php, tools/mvc/compass/personas/post.php, tools/_/app/nav.php, tools/config.production.ini |
|
|
9
10
|
| [Design Demo Admin](features/design-demo-admin.md) | A self-serve admin UI at **`/design`** in the SSO-protected **Tools** app that lets the design team publish self-contained "Claude Design" HTML exports as **ver | tools/_/app/design/github.php, tools/mvc/design/get.php, tools/mvc/design/post.php, tools/assets/css/design.css, tools/assets/js/design.js, tools/_/app/frameworkindex.php, tools/_/app/nav.php, tools/composer.json |
|
|
10
11
|
| [Tools — Developers Folder (UUID & Password Generators)](features/developer-tools.md) | The first two tools shipped in the Tools app, both under the **Developers** folder and gated to personas **Development Team** / **TOGa Technology**. | tools/mvc/developers/uuid/get.php, tools/mvc/developers/password/get.php, tools/mvc/developers/password/post.php |
|
|
11
12
|
| [/errors Curation Console (Tools → shared Core Logs DB)](features/errors-curation-console.md) | Internal-only triage/curation screen for the 2.0 Issue/Event error-reporting pipeline, built as a 1.0 Tools MVC page reading the **shared Core Logs DB** through | tools/mvc/errors/get.php, tools/mvc/errors/post.php, tools/mvc/errors/issue/get.php, tools/assets/css/style.css, tools/_/app/nav.php, tools/config.production.ini |
|
|
@@ -6,8 +6,8 @@ project: Tools
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [jcardinal, kyalamarthi]
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: [jcardinal, kyalamarthi, ajean]
|
|
11
11
|
files:
|
|
12
12
|
- tools/index.php
|
|
13
13
|
- tools/.ebextensions/020_setup_git_libraries.config
|
|
@@ -41,7 +41,11 @@ related:
|
|
|
41
41
|
**Tools** is a standalone 1.0 (`App_`) application that houses many small internal tools behind
|
|
42
42
|
simple interfaces, gated by Client_True staff persona. It is modeled on `togaview` and depends on
|
|
43
43
|
the `library` core. Auth comes via the SAML gateway `?saml=` handoff; the app owns its own
|
|
44
|
-
session and reads `Client_True` **read-only
|
|
44
|
+
session and reads `Client_True` **read-only** — but it is **no longer a read-only app**: as of
|
|
45
|
+
PR tools#13 (unmerged) it also **WRITES** to client tenant databases (`Client_Compass` /
|
|
46
|
+
`Client_CompassCanada`) through new `[database_compass]` / `[database_compasscanada]` ini
|
|
47
|
+
sections that reuse the `[database_true]` account. That is the first non-read-only surface in
|
|
48
|
+
Tools. Adding a tool = create `mvc/<route>/get.php`
|
|
45
49
|
(copy `mvc/_TEMPLATE`) + add one `App_Nav::definition()` entry + `App_Auth::requireAuth([...])`.
|
|
46
50
|
|
|
47
51
|
**Critical rules:** Do NOT use bare `<header>` elements in this app — they inherit the legacy
|
|
@@ -59,6 +63,11 @@ denied at the front controller / out of the web root. New CSS must use `--c-*` t
|
|
|
59
63
|
`App_FrameworkIndex::setHeaderCode()` is the **only** hook for markup in `<head>`, and it
|
|
60
64
|
**SETS rather than appends** — the theme bootstrap is its sole caller, so a second call
|
|
61
65
|
silently clobbers it.
|
|
66
|
+
**Any page that writes to a client tenant DB must `App_Auth::requireAuth()` an explicit, narrow
|
|
67
|
+
persona list (`['Development Team']`) — never `App_Nav::personasForRoute()`**, which returns the
|
|
68
|
+
UNION of the folder's personas and the action's personas and so widens the grant straight back to
|
|
69
|
+
everyone who can see the nav group. Also: **persona checks read a 4-day session snapshot**, so
|
|
70
|
+
removing someone's persona does **not** revoke their access — their session must be killed.
|
|
62
71
|
|
|
63
72
|
## Boot & structure
|
|
64
73
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Compass User & Persona Admin (tools /compass/users, /compass/personas)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: tools
|
|
5
|
+
project: Tools
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: draft
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: [ajean]
|
|
11
|
+
files:
|
|
12
|
+
- tools/_/app/compass.php
|
|
13
|
+
- tools/mvc/compass/users/get.php
|
|
14
|
+
- tools/mvc/compass/users/post.php
|
|
15
|
+
- tools/mvc/compass/personas/get.php
|
|
16
|
+
- tools/mvc/compass/personas/post.php
|
|
17
|
+
- tools/_/app/nav.php
|
|
18
|
+
- tools/config.production.ini
|
|
19
|
+
related:
|
|
20
|
+
- ./mvc-data-access-patterns.md
|
|
21
|
+
- ./persona-gated-navigation.md
|
|
22
|
+
- ./saml-sso-auth.md
|
|
23
|
+
- ../architecture.md
|
|
24
|
+
- ../../../2.0/apps/_underscore/features/effective-persona-resolution.md
|
|
25
|
+
- ../../../clients/compass-usa/features/people-file-user-lifecycle.md
|
|
26
|
+
- ../../../clients/compass-usa/features/persona-model-and-levy-gating.md
|
|
27
|
+
- ../../../clients/compass-usa/workflows/granting-persona-bundle-access.md
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Summary
|
|
31
|
+
|
|
32
|
+
Two pages in the Tools `developers` nav folder that let staff do, in a browser, the two Compass
|
|
33
|
+
requests that used to mean "email a developer, who writes a `dbchanges2` SQL file": **activate /
|
|
34
|
+
deactivate a Compass user**, and **grant a persona**. Both tenants are covered by one tenant
|
|
35
|
+
switcher — **US** (`Client_Compass`) and **Canada** (`Client_CompassCanada`).
|
|
36
|
+
|
|
37
|
+
**This is the first thing in `tools` that WRITES to a client tenant database.** Until now the app
|
|
38
|
+
read `Client_True` read-only. That single fact drives most of the design and every open risk below.
|
|
39
|
+
|
|
40
|
+
Status: built on PR **tools#13**, **not yet merged**. `status: draft` until it ships.
|
|
41
|
+
|
|
42
|
+
## Key files / entry points
|
|
43
|
+
|
|
44
|
+
- **`tools/_/app/compass.php`** — `App_Compass`, all data access for both pages (search, persona
|
|
45
|
+
resolution, activate/deactivate, grant).
|
|
46
|
+
- **`tools/mvc/compass/users/{get,post}.php`** — user search + activate/deactivate + grant persona.
|
|
47
|
+
- **`tools/mvc/compass/personas/{get,post}.php`** — inspect one persona and who reaches it.
|
|
48
|
+
- **`tools/_/app/nav.php`** — the two nav entries, inside the existing `developers` folder.
|
|
49
|
+
- **`tools/config.production.ini`** — new `[database_compass]` / `[database_compasscanada]` sections.
|
|
50
|
+
|
|
51
|
+
## How it works
|
|
52
|
+
|
|
53
|
+
### What the pages do
|
|
54
|
+
|
|
55
|
+
1. **Find a user** by `c_hrEmpUsername`, `email`, or `c_hrEmpPersonnelNbr`.
|
|
56
|
+
2. **Activate / deactivate** them (`Users.isActive`).
|
|
57
|
+
3. **See the personas that user reaches, and WHY** — direct row, own location, or sector.
|
|
58
|
+
4. **Inspect a persona** and list who reaches it, from all three sources.
|
|
59
|
+
5. **Grant a persona** — one `INSERT` into `Users_Personas`.
|
|
60
|
+
|
|
61
|
+
The "why" column is the point of the tool. Answering "who has persona N" from `Users_Personas`
|
|
62
|
+
alone badly under-reports on Compass, because most users hold their catalogue through their
|
|
63
|
+
**sector**, not through a row of their own. The resolution rule the tool implements is the
|
|
64
|
+
framework's own — see
|
|
65
|
+
[Effective persona resolution](../../../2.0/apps/_underscore/features/effective-persona-resolution.md),
|
|
66
|
+
which also carries the measured prod numbers and the "reverse lookups must be set-based" rule.
|
|
67
|
+
|
|
68
|
+
### Access gating is deliberately tighter than the folder
|
|
69
|
+
|
|
70
|
+
Both pages `requireAuth(['Development Team'])` — narrower than the `developers` folder's own
|
|
71
|
+
persona set. That is on purpose: these are tenant **writes**, not read-only developer utilities.
|
|
72
|
+
Passing `App_Nav::personasForRoute()` here would widen the grant back to the folder's set; see the
|
|
73
|
+
nav-narrowing gotcha in [MVC data access patterns](./mvc-data-access-patterns.md).
|
|
74
|
+
|
|
75
|
+
### Database connections
|
|
76
|
+
|
|
77
|
+
New `[database_compass]` and `[database_compasscanada]` sections in `config.production.ini` reuse the
|
|
78
|
+
**existing `[database_true]` account** — same client cluster, only `dbname` differs. **No new secret
|
|
79
|
+
value was introduced.** See the accepted risk below.
|
|
80
|
+
|
|
81
|
+
### Schema footprint: none
|
|
82
|
+
|
|
83
|
+
The tool creates and alters **nothing**. It reads/writes only `Users`, `Users_Personas`,
|
|
84
|
+
`Locations_Personas`, `Locations`, and reads `Personas` for names.
|
|
85
|
+
|
|
86
|
+
## Decisions
|
|
87
|
+
|
|
88
|
+
- **2026-09-04 — No new columns, no new tables (Jeff Cardinal, back-end review).** The tool talks
|
|
89
|
+
directly to production `Client_Compass` / `Client_CompassCanada` using the existing schema only.
|
|
90
|
+
This killed an earlier design that added `Users.c_isKeepActive` plus expiry/audit columns, an
|
|
91
|
+
`AdminAuditLogs` table, and three search indexes. `dbchanges2#478` and `worker2#160` were opened
|
|
92
|
+
and then **closed unmerged** — do not resurrect them without re-opening that decision.
|
|
93
|
+
- **2026-09-04 — Reuse the `[database_true]` credential** rather than split out a least-privilege
|
|
94
|
+
Compass account. Raised twice in security review and consciously declined for now (see below).
|
|
95
|
+
|
|
96
|
+
## Gotchas
|
|
97
|
+
|
|
98
|
+
- **⚠ Activating a Compass user is TEMPORARY; deactivating is durable.** Because the
|
|
99
|
+
`c_isKeepActive` flag was dropped from scope, the nightly PEOPLE importer will deactivate anyone
|
|
100
|
+
absent from that day's file — so an activation only sticks if the person is actually in the file.
|
|
101
|
+
Deactivation holds, because the cron never reactivates anybody. Full lifecycle:
|
|
102
|
+
[PEOPLE-file User Lifecycle](../../../clients/compass-usa/features/people-file-user-lifecycle.md).
|
|
103
|
+
- **A cross-region user still needs a code change and a deploy.** The only durable way to hold one
|
|
104
|
+
active is the hardcoded four-address list in `ensureCrossRegionUsersActive()` in
|
|
105
|
+
`worker2/Worker/Client/Compass/PeopleFile.php`. This tool does not change that.
|
|
106
|
+
- **Every user search is a full table scan.** `Client_Compass.Users` has no index on `email`,
|
|
107
|
+
`c_hrEmpUsername`, `c_hrEmpPersonnelNbr` or `isActive` (176k rows; `EXPLAIN` reports `type=ALL`,
|
|
108
|
+
no possible keys). Same on `Client_CompassCanada`. Keep result sets bounded and do not add
|
|
109
|
+
per-row lookups against those columns in a loop.
|
|
110
|
+
- **⚠ Accepted risk — the tool widens the blast radius of one shared credential.** It points the
|
|
111
|
+
`[database_true]` account at two more tenant schemas. `config.production.ini` is committed and was
|
|
112
|
+
web-readable until the 2026-07-24 `FilesMatch` deny blocks, so **rotation of that account is still
|
|
113
|
+
owed**. An inline comment in the ini records this and lists the exact grants a split-out Compass
|
|
114
|
+
account would need. Never paste the value anywhere.
|
|
115
|
+
- **⚠ Persona revocation does not take effect for up to 4 days.** Tools snapshots persona names into
|
|
116
|
+
`$_SESSION` at login and sessions last 4 days, so removing someone's `Development Team` persona
|
|
117
|
+
does not lock them out of these pages until their session dies. See
|
|
118
|
+
[SAML SSO & persona-gated auth](./saml-sso-auth.md).
|
|
119
|
+
- **No staff-actor audit trail exists for these writes.** `Logs_Compass.Record.userId` is a **tenant**
|
|
120
|
+
`Users.id`, so it cannot record a `Client_True` staff member — it is not usable as the audit log
|
|
121
|
+
for this tool, and `Logs_Compass.Record` is the client-facing activity log (3.6M rows) besides.
|
|
122
|
+
See
|
|
123
|
+
[Record Change Audit Log](../../../2.0/apps/_underscore/features/record-change-audit-log.md).
|
|
124
|
+
Auditing internal-tool actions remains an open item.
|
|
125
|
+
- **Detecting a no-op UPDATE needs `App_Database::affectedRows()`** — `App_Database::query()` returns
|
|
126
|
+
the `App_Query` execute result, not a row count. Details in
|
|
127
|
+
[MVC data access patterns](./mvc-data-access-patterns.md).
|
|
128
|
+
|
|
129
|
+
## Change history
|
|
130
|
+
|
|
131
|
+
- 2026-09-04 — Initial: built the two-page Compass user/persona admin (PR tools#13, unmerged) — the
|
|
132
|
+
first `tools` pages that write to a client tenant DB, gated to `Development Team` only, with a
|
|
133
|
+
US/Canada switcher and new `[database_compass]` / `[database_compasscanada]` ini sections reusing
|
|
134
|
+
the existing `[database_true]` account. Recorded Jeff Cardinal's scope reset (no schema changes;
|
|
135
|
+
`dbchanges2#478` and `worker2#160` closed unmerged) and its consequence — **activation is temporary
|
|
136
|
+
because the `c_isKeepActive` flag was dropped**, while deactivation is durable. Logged the accepted
|
|
137
|
+
credential-reuse risk, the 4-day session persona staleness, the missing staff-actor audit trail, and
|
|
138
|
+
that every user search is a full scan. (ajean)
|
|
@@ -6,8 +6,8 @@ project: Tools
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [jcardinal, bala]
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: [jcardinal, bala, ajean]
|
|
11
11
|
files:
|
|
12
12
|
- tools/_/app/nav.php
|
|
13
13
|
- tools/mvc/get.php
|
|
@@ -15,6 +15,7 @@ files:
|
|
|
15
15
|
- library/app/database.php
|
|
16
16
|
related:
|
|
17
17
|
- ./persona-gated-navigation.md
|
|
18
|
+
- ./compass-user-persona-admin.md
|
|
18
19
|
- ./saml-sso-auth.md
|
|
19
20
|
- ../architecture.md
|
|
20
21
|
- ../../library/features/mvc-page-pattern-and-app-skeleton.md
|
|
@@ -57,6 +58,31 @@ add one expecting the framework to validate it.
|
|
|
57
58
|
2.0 DB read-only through its own connection (e.g. `db_true → Client_True`); adding
|
|
58
59
|
`db_team → Team` is the same pattern.
|
|
59
60
|
|
|
61
|
+
### Detecting a no-op UPDATE — `App_Database::affectedRows()`
|
|
62
|
+
`App_Database::query()` returns the `App_Query` execute result, **not** a row count, so it cannot
|
|
63
|
+
tell you whether an `UPDATE` actually changed anything. Use
|
|
64
|
+
`App_Database::affectedRows($linkRegistryString)` — pass the same alias you queried with. This is
|
|
65
|
+
how a page distinguishes "saved" from "the value was already that".
|
|
66
|
+
|
|
67
|
+
### DB credentials come straight from the ini — there is no secret store
|
|
68
|
+
`App_Database::registerDatabaseConnect()` (`library/app/database.php:125-128`) reads
|
|
69
|
+
**hostname / username / password / dbname directly from the config ini**. Only
|
|
70
|
+
`database_reader_endpoint` goes through `App_Cloud::getParameter()`. So a 1.0 app **cannot** move a
|
|
71
|
+
DB credential into a secret store without changing shared `library` core that every 1.0 app boots
|
|
72
|
+
through. Plan around it: `config.*.ini` is where DB credentials live, and it is committed (see the
|
|
73
|
+
config/secrets note in [the architecture doc](../architecture.md)).
|
|
74
|
+
|
|
75
|
+
### Input filtering — `App_Page::getVarIfSet()` allows printable ASCII
|
|
76
|
+
`getVarIfSet()` runs `App_String::filterCharactersValidCharactersOnly()`, which permits **ASCII
|
|
77
|
+
32-126**. Emails, usernames and personnel numbers survive intact, so it is safe for those search
|
|
78
|
+
inputs. It is a character filter, not an escaper — still escape/cast before the value reaches SQL.
|
|
79
|
+
|
|
80
|
+
### Production MySQL runs `US/Central`
|
|
81
|
+
The prod MySQL server timezone is **`US/Central`**, matching the 1.0 framework's `America/Chicago`
|
|
82
|
+
and worker2's Central-time cron scheduling (verified 2026-09-02). So MySQL `CURDATE()` and PHP
|
|
83
|
+
`date('Y-m-d')` agree — there is **no** day-boundary divergence to design around when the same date
|
|
84
|
+
check exists in both SQL and PHP.
|
|
85
|
+
|
|
60
86
|
### Gotcha — by-reference row helpers
|
|
61
87
|
`buildArrayOfRows`, `fetchOne`, and `fetchRow` take `$res` **by reference**. Assign the
|
|
62
88
|
`query()` result to a **variable first**, then pass it — passing the call result inline emits
|
|
@@ -142,6 +168,14 @@ set_error_handler(fn(): bool => true); // swallow non-fatal notices/deprecations
|
|
|
142
168
|
`App_Talos_Estimator` → `_/app/talos/estimator.php`.
|
|
143
169
|
|
|
144
170
|
## Change history
|
|
171
|
+
- 2026-09-04 — Added four `App_Database` / input facts found building the Compass admin pages:
|
|
172
|
+
`affectedRows($alias)` is how you detect a no-op `UPDATE` (`query()` returns the `App_Query`
|
|
173
|
+
execute result, not a row count); `registerDatabaseConnect()` reads DB credentials **directly from
|
|
174
|
+
the config ini** (only `database_reader_endpoint` uses `App_Cloud::getParameter()`), so there is no
|
|
175
|
+
secret-store option without changing shared 1.0 core; `App_Page::getVarIfSet()` filters to
|
|
176
|
+
printable ASCII 32-126, so emails/usernames/personnel numbers pass through intact; and production
|
|
177
|
+
MySQL runs timezone **`US/Central`**, matching PHP's `America/Chicago`, so `CURDATE()` and
|
|
178
|
+
`date('Y-m-d')` agree. (ajean)
|
|
145
179
|
- 2026-08-04 — Added two gotchas found rebuilding the Talos Pricing UI: (1) `App_Database::fetchOne()` is **not** an existence test — it `data_seek`s and returns `$row[$col]` with no empty check, so zero rows dereferences null → uncaught `ErrorException` → bare 500; a **library-wide** footgun affecting every 1.0 app (use `COUNT(*)` or `buildArrayOfRows` + `empty()`). (2) Tools runs **PHP 8.5**, where "using null as an array offset" is deprecated even inside `isset()` and the framework escalates deprecations to fatals — cast to `(string)` before indexing; PHP 8.2 local dev does not reproduce it. (jcardinal)
|
|
146
180
|
- 2026-08-03 — Added the error-handler escalation gotcha: `App_Error::handleError` promotes ANY PHP
|
|
147
181
|
notice/deprecation to a thrown `ErrorException` (ignoring `error_reporting`) and, without Sentry,
|
|
@@ -6,8 +6,8 @@ project: Tools
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [jcardinal]
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: [jcardinal, ajean]
|
|
11
11
|
files:
|
|
12
12
|
- tools/_/app/auth.php
|
|
13
13
|
- tools/mvc/sso/initiate/get.php
|
|
@@ -20,6 +20,7 @@ files:
|
|
|
20
20
|
- tools/config.local.ini
|
|
21
21
|
related:
|
|
22
22
|
- ../architecture.md
|
|
23
|
+
- ./compass-user-persona-admin.md
|
|
23
24
|
- ../../library/features/app-sso-initiation.md
|
|
24
25
|
- ../../../2.0/apps/saml/features/downstream-integration-contract.md
|
|
25
26
|
- ../../../clients/true/features/users-personas-data-model.md
|
|
@@ -115,6 +116,17 @@ removed.
|
|
|
115
116
|
|
|
116
117
|
## Gotchas / known issues
|
|
117
118
|
|
|
119
|
+
- **⚠ Personas are snapshotted at login and sessions last 4 days — revoking a persona does not
|
|
120
|
+
revoke access.** `App_Auth` caches the user's persona **names** into `$_SESSION` at sign-in and
|
|
121
|
+
`requireAuth()` / `App_Nav` both read that cache, never the live `Users_Personas` rows. So removing
|
|
122
|
+
someone's persona leaves them able to use every page it gated for up to **4 days**, until their
|
|
123
|
+
session expires. This matters most for pages that write to a client tenant — see the [Compass
|
|
124
|
+
admin pages](./compass-user-persona-admin.md). To cut access off now, kill the session, do not just
|
|
125
|
+
revoke the persona.
|
|
126
|
+
- **The Tools session cookie has no `Secure` flag.** `App_Framework_Tools` sets `useSessions = false`,
|
|
127
|
+
so the parent `library` framework's `cookie_secure` setup never runs; Tools sets the cookie itself
|
|
128
|
+
with HttpOnly + SameSite=Lax only. Open item — do not assume the cookie is HTTPS-only.
|
|
129
|
+
|
|
118
130
|
- **Consumer route runs mid-render → headers already sent.** The 1.0 framework runs `mvc/sso/get.php`
|
|
119
131
|
**inside** the page render (`frameworkindex → body → loadFile`) after `common/header.php` has
|
|
120
132
|
emitted output, so `session_regenerate_id(true)` fatals ("cannot be regenerated after headers
|
|
@@ -180,6 +192,11 @@ remains (already noted in tools knowledge) — **flag for rotation.** Document *
|
|
|
180
192
|
never the values.
|
|
181
193
|
|
|
182
194
|
## Change history
|
|
195
|
+
- 2026-09-04 — Recorded two standing security gaps found while adding tenant-writing pages: personas
|
|
196
|
+
are **snapshotted into `$_SESSION` at login** and sessions last **4 days**, so revoking a persona
|
|
197
|
+
does not revoke page access until the session dies (kill the session instead); and the Tools
|
|
198
|
+
session cookie carries **no `Secure` flag**, because `App_Framework_Tools` sets `useSessions =
|
|
199
|
+
false` and the parent framework's `cookie_secure` setup therefore never runs. (ajean)
|
|
183
200
|
- 2026-06-30 — Fixed SSO "Sign in failed" (`/login?error=1`) regression and **corrected the earlier wrong "config tokens are removable" note**. The prior fix moved only the consumer/decrypt leg to `Core.Parameters` and advised the `[saml] api_secret_access_token` was removable — but the **initiate** leg (`mvc/sso/initiate/get.php`) still read it from config, so removing it made `App_Sso::initiate()` throw on an empty token (`library/app/sso.php:64-65`) → `/login?error=1`. Fix: **both** legs now source the rotating secret from `Core.Parameters` — added public `App_Auth::currentApiSecret()` (reuses cached `handoffKeys()`), and `sso/initiate/get.php` passes it instead of the config value. Documented the non-empty-token requirement, the `/login?error=1` (initiate) vs "Authentication failed" (consumer) leg distinction, the `true_client_uuid`-stays-in-config rule, and the `db_toga2core`-missing→500 diagnostic. `php -l` clean on both files. (jcardinal)
|
|
184
201
|
- 2026-06-30 — Fixed prod SSO login failure: handoff decrypt keys now sourced from `Core.Parameters` (rotated `API_SECRET_ACCESS_TOKEN`/`_PREVIOUS`) via new `App_Auth::handoffKeys()` over `db_toga2core` (per-request cached, current→previous fallback), instead of the stale committed `config [saml]` tokens — the stale key was the root cause. Added `[database_toga2core]` connection requirement to every config and documented the AES-256-CBC crypto format. Fixed `tools_ssoFail()` 500→intended 401: `http_response_code(401)` fataled on headers-already-sent (preloader flushes before mvc page runs); guarded with `!headers_sent()`. (jcardinal)
|
|
185
202
|
- 2026-06-26 — Wired up real SSO initiation via the new 1.0 `App_Sso` library class (`/sso/initiate`), replacing the `initiation_url` placeholder; registered the fixed-uuid Core.Domains return row (dbchanges2 `2026-06-26a`); fixed the `session_regenerate_id` headers-already-sent fatal (guarded with `!headers_sent()`) and documented the mid-render failure-path limitation; collapsed home → `/login` to one-step sign-in; noted prod secrets live in `config.production.ini [saml]` (jcardinal)
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
| [_Config group access — the two-argument "optional" form does NOT rescue a missing GROUP](features/config-group-access.md) | `_Config::<group>('<property>')` reads a value out of the active `Config/<ENVIRONMENT>.ini`, and **it throws when the requested group is absent from the ini.** | _underscore/Config.php, _underscore/Component/Api/Paypal/Paypal.php, _underscore/Worker.php, api2/Config/beta.ini, api2/Config/sandbox-dev.ini |
|
|
20
20
|
| [Core.Domains — the app host registry (and why you cannot derive a host)](features/core-domains-app-host-registry.md) | `Core.Domains` is the **authoritative registry of which host serves which client's app in which environment**. | _underscore/Model/Core/Domain.php, api2/Component/Api/CrossClient/CrossClient.php, api2/Component/Api/V2/V2.php, worker/config.beta.ini |
|
|
21
21
|
| [Re-pointing a DB alias mid-request (_Database::register park/restore)](features/database-alias-repointing.md) | `_Database` keys **all live per-database runtime state by the connection ALIAS** (`Client` / `_underscore::DB_CLIENT`, `ClientLogs`, `Archive`), **not** by the | _underscore/Database.php, _underscore/Query.php, api2/Component/Api/V2/V2.php, api2/Component/Api/CrossClient/CrossClient.php |
|
|
22
|
+
| [Effective persona resolution (_Model_Client_User::_personaIds) — the three sources and the sector hop](features/effective-persona-resolution.md) | A user's **effective** persona set is **not** `Users_Personas`. | _underscore/Model/Client/User.php |
|
|
22
23
|
| [2.0 Email Send Pipeline (queue + Send worker)](features/email-send-pipeline.md) | In 2.0, `_Email::send()` **does not transmit** — it queues the message. | _underscore/Email.php, _underscore/String.php, worker2/Worker/Infrastructure/Email/Send.php, _underscore/Model/Client/Logs/Email.php, _underscore/Model/Client/Logs/EmailAttachment.php |
|
|
23
24
|
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Quad/ApprovalDecision.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
24
25
|
| [Error Reporting — Issue/Event Capture, Fingerprinting & Aggregation](features/error-reporting-issue-event.md) | Platform-wide error reporting for TOGA 2.0, built on an **Issue / Event** aggregation model in the **shared Core Logs DB**. | _underscore/Error.php, _underscore/Query.php, _underscore/Database.php, _underscore/Exception/Business.php, _underscore/Exception/Database.php, _underscore/Test/DatabaseFingerprint.php, api2/Controller/Index.php, worker2/Controller/Index.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, _underscore/Model/Core/Logs/IssueFingerprint.php, _underscore/Model/Core/Logs/IssueClickupTask.php, _underscore/Model/Core/Logs/IssueEmailAddress.php, _underscore/Model/Core/Logs/IssueAreaOwner.php, worker2/Worker/Infrastructure/Errors.php, tools/mvc/errors/issue/get.php, tools/mvc/errors/post.php, dbchanges2/Logs/2026-07-30a - Error reporting Issues and Events.sql, dbchanges2/Logs/2026-08-01a - Rename tables to singular.sql, dbchanges2/Logs_Client/2026-08-01a - Drop Error table.sql, dbchanges2/Core/2026-07-30a - Error escalation cron job.sql |
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Effective persona resolution (_Model_Client_User::_personaIds) — the three sources and the sector hop
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: [ajean]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/User.php
|
|
13
|
+
related:
|
|
14
|
+
- ./persona-name-translation.md
|
|
15
|
+
- ../../../clients/compass-usa/features/persona-model-and-levy-gating.md
|
|
16
|
+
- ../../../clients/compass-usa/workflows/granting-persona-bundle-access.md
|
|
17
|
+
- ../../../clients/compass-usa/workflows/persona-population-env-comparison.md
|
|
18
|
+
- ../../../1.0/apps/tools/features/compass-user-persona-admin.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Summary
|
|
22
|
+
|
|
23
|
+
A user's **effective** persona set is **not** `Users_Personas`. The framework already owns the rule:
|
|
24
|
+
`_Model_Client_User::_personaIds()` (`_underscore/Model/Client/User.php:48`) is the **source of
|
|
25
|
+
truth**. It is a `FIELD_SQL` virtual column — a `GROUP_CONCAT(personaId SEPARATOR ':')` over a
|
|
26
|
+
**UNION of three sources**.
|
|
27
|
+
|
|
28
|
+
Anything that answers "what can this user see?" or "who can see persona N?" must reproduce all three
|
|
29
|
+
sources. Reading `Users_Personas` alone badly under-reports: on Compass, the overwhelming majority of
|
|
30
|
+
users hold their catalogue through their **sector**, not through a row of their own.
|
|
31
|
+
|
|
32
|
+
## How it works
|
|
33
|
+
|
|
34
|
+
### The three sources
|
|
35
|
+
|
|
36
|
+
1. **`Users_Personas`** on the user's own `userId` — personas granted directly.
|
|
37
|
+
2. **`Locations_Personas`** on the user's **own** `locationId`.
|
|
38
|
+
3. **`Locations_Personas`** on the user's **sector** — reached by **five INNER JOINs** up the
|
|
39
|
+
`Locations` chain, with semantic aliases:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Locations -> LocationComplex -> LocationDistrict -> LocationRegion -> LocationDivision -> LocationSector
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Because those five hops are `INNER JOIN`s, a user whose chain is shorter than five levels simply
|
|
46
|
+
contributes nothing from source 3 — no error, just an empty branch.
|
|
47
|
+
|
|
48
|
+
### Measured on production, 2026-09-02
|
|
49
|
+
|
|
50
|
+
| Tenant | Active users | Resolve at hop 5 (sector) | Resolve at hop 0 (own location) | Hops 1-4 and 6 |
|
|
51
|
+
|---|---|---|---|---|
|
|
52
|
+
| `Client_Compass` | 74,268 | **67,157** | 338 | **0** |
|
|
53
|
+
| `Client_CompassCanada` | 4,948 | **4,029** | — | **0** |
|
|
54
|
+
|
|
55
|
+
So in practice only **two** of the possible depths ever pay out: the user's own location, and the
|
|
56
|
+
five-hop sector. Nothing in between.
|
|
57
|
+
|
|
58
|
+
**But "persona locations all sit at one level" is FALSE.** The 123 persona-bearing locations in
|
|
59
|
+
`Client_Compass` sit at two different depths — 15 with 3 ancestors, 108 with 6 or more. The depths
|
|
60
|
+
that *users* pick them up at are stable; the depths the *locations* live at are not. Do not write a
|
|
61
|
+
query that assumes a fixed hierarchy depth for `Locations_Personas`.
|
|
62
|
+
|
|
63
|
+
## Gotchas
|
|
64
|
+
|
|
65
|
+
- **⚠ A reverse lookup (persona → users) must be SET-BASED.** Build it as the union of the three
|
|
66
|
+
sources and join once. Never run an `EXISTS`-per-user check: on `Client_Compass` that is 74k
|
|
67
|
+
executions of a five-join chain against a `Users` table with no useful index (see below).
|
|
68
|
+
- **`Users_Personas` alone is the wrong answer.** On Compass it would return roughly 338 of the
|
|
69
|
+
67,495 users who actually reach a sector-granted persona. Any "who has persona N" report built on
|
|
70
|
+
it is wrong, not merely incomplete.
|
|
71
|
+
- **The rule lives in the framework — do not re-derive it.** If you are writing SQL for a report, a
|
|
72
|
+
migration, or an internal tool, mirror `_personaIds()` rather than inventing a resolution. Where
|
|
73
|
+
the effective set matters for a *grant*, the same three-source check is the pre-flight step in
|
|
74
|
+
[Granting a Compass user access to a bundle](../../../clients/compass-usa/workflows/granting-persona-bundle-access.md)
|
|
75
|
+
— the user may already reach the persona, in which case the right answer is "no change needed".
|
|
76
|
+
- **No index backs the user side of these queries on Compass.** `Client_Compass.Users` has no index
|
|
77
|
+
on `email`, `c_hrEmpUsername`, `c_hrEmpPersonnelNbr` or `isActive`, so every lookup is a full scan
|
|
78
|
+
(`EXPLAIN` → `type=ALL`, 176,018 rows, no possible keys). Same on `Client_CompassCanada`.
|
|
79
|
+
|
|
80
|
+
## Change history
|
|
81
|
+
|
|
82
|
+
- 2026-09-04 — Initial: documented `_Model_Client_User::_personaIds()` as the source of truth for a
|
|
83
|
+
user's effective persona set — a `FIELD_SQL` `GROUP_CONCAT` over a UNION of `Users_Personas`, the
|
|
84
|
+
user's own `Locations_Personas`, and their **sector's**, reached by five aliased INNER JOINs.
|
|
85
|
+
Measured on prod 2026-09-02: 67,157 of 74,268 active `Client_Compass` users resolve at hop 5 and
|
|
86
|
+
only 338 at hop 0, with **zero** at any other hop (Canada: 4,029 of 4,948 at hop 5). Recorded that
|
|
87
|
+
persona-bearing locations sit at two different depths, so "one level" assumptions are wrong, and
|
|
88
|
+
that reverse persona→user queries must be set-based, never EXISTS-per-user. (ajean)
|
|
@@ -6,14 +6,15 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [bala]
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: [bala, ajean]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/Logs/Record.php
|
|
13
13
|
- _underscore/Model/Client/Logs/RecordField.php
|
|
14
14
|
- _underscore/Model/Client/Logs/CustomRecordField.php
|
|
15
15
|
related:
|
|
16
16
|
- ../../api2/features/request-logging.md
|
|
17
|
+
- ../../../1.0/apps/tools/features/compass-user-persona-admin.md
|
|
17
18
|
- ../../../../clients/compass-usa/features/approval-decision-flow.md
|
|
18
19
|
- ../../../../clients/compass-usa/features/stranded-approval-reassignment.md
|
|
19
20
|
- ../../api2/features/api-payload-interceptors.md
|
|
@@ -106,6 +107,15 @@ single record: the milestone notes are the chapter headings, the field changes a
|
|
|
106
107
|
to read the client id from — resolve it from `Core` (`Clients` JOIN `Databases` on `name`). Known
|
|
107
108
|
ids: `Client_Compass` = **2** (`Logs_Compass`), `Client_CompassCanada` = **43**
|
|
108
109
|
(`Logs_CompassCanada`). This applies to **every** client, not just Compass.
|
|
110
|
+
- **⚠ `Record.userId` is a TENANT `Users.id` — it cannot record an internal staff actor.** There is
|
|
111
|
+
no way to express "a `Client_True` staff member did this" in this table, so it is **not** an audit
|
|
112
|
+
log for internal-tool actions taken against a client tenant (e.g. the [Tools Compass admin
|
|
113
|
+
pages](../../../1.0/apps/tools/features/compass-user-persona-admin.md)). It is also the
|
|
114
|
+
**client-facing** activity log — `Logs_Compass.Record` is ~3.6M rows — so staff actions do not
|
|
115
|
+
belong in it even if the id could be made to fit. An internal audit trail needs its own home.
|
|
116
|
+
- **Tables in the `Logs_<Tenant>` schema are SINGULAR — the documented exception to the plural table
|
|
117
|
+
convention.** `Logs_Compass` holds `Api`, `CustomRecordField`, `Record`, `RecordField`, `Email`,
|
|
118
|
+
`EmailAttachment`. If you ever add a log table there, name it singular to match.
|
|
109
119
|
- **⚠ `Logs_*` and `Core` are on different database clusters — you cannot join them in one query.**
|
|
110
120
|
`Record.recordId` → `Core.Records` and `RecordField.recordFieldId` → `Core.RecordFields` are
|
|
111
121
|
logical foreign keys across a cluster boundary. Resolve the `Core` ids in a **separate** query and
|
|
@@ -119,7 +129,12 @@ single record: the milestone notes are the chapter headings, the field changes a
|
|
|
119
129
|
mechanism, not the people.
|
|
120
130
|
|
|
121
131
|
## Change history
|
|
122
|
-
|
|
132
|
+
- 2026-09-04 — Recorded two facts found while looking for an audit home for an internal admin tool:
|
|
133
|
+
`Record.userId` is a **tenant** `Users.id`, so this log **cannot** record a `Client_True` staff
|
|
134
|
+
actor and is not the audit trail for internal-tool writes (and `Logs_Compass.Record` is the
|
|
135
|
+
~3.6M-row client-facing activity log besides); and tables in the `Logs_<Tenant>` schema are
|
|
136
|
+
**singular** (`Api`, `CustomRecordField`, `Record`, `RecordField`, `Email`, `EmailAttachment`) —
|
|
137
|
+
the documented exception to the plural table convention. (ajean)
|
|
123
138
|
- 2026-08-20 — Added the **`addNote()` is a silent no-op outside api2** gotcha: `addNote()` only
|
|
124
139
|
stages into the static `$_notes` array and **only api2's `Component/Api/V2/V2.php` ever flushes
|
|
125
140
|
it**, so every `addNote()` call from `_underscore` or `worker2` loses its note with no error. A
|
|
@@ -6,8 +6,8 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: [bala, kyalamarthi, apeterson]
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: [bala, kyalamarthi, apeterson, ajean]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
|
|
13
13
|
- dbchanges2/Core/2026-08-27a - Insert - Netsuite Location SyncAll CronJob.sql
|
|
@@ -220,9 +220,13 @@ PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
|
|
220
220
|
|
|
221
221
|
- **`TABLE_SCHEMA = DATABASE()`** keeps it cluster-safe — the file never names a database, so the
|
|
222
222
|
same statement is correct in every `Client_*` schema.
|
|
223
|
-
- Guard **indexes** the same way against `information_schema.STATISTICS
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
- Guard **indexes** the same way against `information_schema.STATISTICS`, but **key the guard on
|
|
224
|
+
`COLUMN_NAME` + `SEQ_IN_INDEX = 1`, never on `INDEX_NAME`.** An index name is arbitrary: guarding
|
|
225
|
+
on the name misses an equivalent index that someone already created under a different name, and
|
|
226
|
+
you silently add a duplicate. Guarding on "is this column already the first column of some index"
|
|
227
|
+
asks the question you actually mean. (A composite unique key is likewise only recorded against its
|
|
228
|
+
first column — see
|
|
229
|
+
[tableview-field-metadata](../../api2/features/tableview-field-metadata.md).)
|
|
226
230
|
- The `PREPARE` shape is what makes the guard work at all: **DDL cannot sit inside an `IF`** in a
|
|
227
231
|
plain script, so the condition has to select the *statement text*, not branch around it.
|
|
228
232
|
|
|
@@ -280,6 +284,11 @@ concluding a migration is production-safe.
|
|
|
280
284
|
[FIELD_SQL calculated fields](../../_underscore/features/calculated-sql-fields.md).
|
|
281
285
|
|
|
282
286
|
## Change history
|
|
287
|
+
- 2026-09-04 — Sharpened the index guard: check `information_schema.STATISTICS` on **`COLUMN_NAME` +
|
|
288
|
+
`SEQ_IN_INDEX = 1`**, not on `INDEX_NAME` — an existing index under a different name goes
|
|
289
|
+
undetected by a name-based guard and you add a duplicate. Found while sizing (and then dropping)
|
|
290
|
+
the Compass `Users` search indexes. (ajean)
|
|
291
|
+
|
|
283
292
|
- 2026-09-01 — Added the **fan-out rollback rule**: a `Client/` grant migration that fans out across
|
|
284
293
|
every tenant must use **hardcoded v4 uuid literals**, not the generator, so its rollback
|
|
285
294
|
(`DELETE … WHERE uuid IN (…)`) can only touch rows that file created. A natural-key rollback on
|
package/knowledge/INDEX.md
CHANGED
|
@@ -14,11 +14,11 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
14
14
|
- **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
|
|
15
15
|
- **test** (Test) — 15 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
|
|
16
16
|
- **toga** (TOGa) — 3 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
|
|
17
|
-
- **tools** (Tools) —
|
|
17
|
+
- **tools** (Tools) — 17 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
|
|
18
18
|
|
|
19
19
|
## 2.0 framework
|
|
20
20
|
|
|
21
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
21
|
+
- **_underscore** (_Underscore) _(framework core)_ — 80 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
22
|
- **worker2** (Worker) — 59 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
23
|
- **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
24
|
- **dbchanges2** (Database Changes) _(framework core)_ — 15 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -12,12 +12,13 @@ apps:
|
|
|
12
12
|
- worker2
|
|
13
13
|
- dbchanges2
|
|
14
14
|
- library
|
|
15
|
+
- tools
|
|
15
16
|
project: _Underscore
|
|
16
17
|
client: compass-canada
|
|
17
18
|
type: profile
|
|
18
19
|
status: active
|
|
19
|
-
updated: 2026-09-
|
|
20
|
-
owners: [jcardinal, bala, tcox, apeterson]
|
|
20
|
+
updated: 2026-09-04
|
|
21
|
+
owners: [jcardinal, bala, tcox, apeterson, ajean]
|
|
21
22
|
files: []
|
|
22
23
|
related:
|
|
23
24
|
- ../compass-usa/profile.md
|
|
@@ -6,12 +6,13 @@ project: Worker
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [bala]
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: [bala, ajean]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Client/Compass/PeopleFile.php
|
|
13
13
|
related:
|
|
14
14
|
- ./persona-model-and-levy-gating.md
|
|
15
|
+
- ../../../1.0/apps/tools/features/compass-user-persona-admin.md
|
|
15
16
|
- ./approval-decision-flow.md
|
|
16
17
|
- ./stranded-approval-reassignment.md
|
|
17
18
|
- ../profile.md
|
|
@@ -118,9 +119,27 @@ rediscovered, and the deactivation path has no model hooks at all (see below).
|
|
|
118
119
|
- **`_Model_Client_Logs_Record::addNote()` is a no-op in this cron.** Staged notes are only flushed
|
|
119
120
|
by api2, so a worker must write its `Record` rows itself. See
|
|
120
121
|
[Record Change Audit Log](../../../2.0/apps/_underscore/features/record-change-audit-log.md).
|
|
121
|
-
- **`Client_Compass.Users` is 176,
|
|
122
|
-
`isActive`.** Any per-row lookup you add against those columns is a full
|
|
123
|
-
|
|
122
|
+
- **`Client_Compass.Users` is ~176,000 rows with no index on `c_hrEmpPersonnelNbr`, `email`,
|
|
123
|
+
`c_hrEmpUsername`, or `isActive`.** Any per-row lookup you add against those columns is a full
|
|
124
|
+
scan — `EXPLAIN` returns `type=ALL`, 176,018 rows, no possible keys (re-confirmed on prod
|
|
125
|
+
2026-09-02, and the same on `Client_CompassCanada`). An index migration is recommended and **still
|
|
126
|
+
not done**. **The blank-client template is not a safe proxy for this schema**: `dbchanges2/Client/
|
|
127
|
+
2026-06-03- BLANK_CLIENT_DATABASE.sql` declares `UNIQUE KEY email (email)`, and Compass has
|
|
128
|
+
diverged from it. Check the live tenant, never the template.
|
|
129
|
+
- **⚠ `executeForTenant()`'s inactivation sweep is the ONLY place in the file that writes
|
|
130
|
+
`isActive = 0`.** Every other `isActive` path in the ~2,200-line class only forces it back to
|
|
131
|
+
**1**. Two consequences: (a) **an activation made outside this cron is temporary** — the next run
|
|
132
|
+
deactivates the person again unless they are in that day's PEOPLE file — while (b) **a
|
|
133
|
+
deactivation is durable**, because nothing here ever reactivates anyone on its own. This is what
|
|
134
|
+
makes the [Tools Compass admin
|
|
135
|
+
pages](../../../1.0/apps/tools/features/compass-user-persona-admin.md) a one-way-durable tool.
|
|
136
|
+
- **`ensureCrossRegionUsersActive()` runs `UPDATE Users SET isActive = 1 WHERE email = X` with no
|
|
137
|
+
`isActive` filter**, so it reactivates **every** row carrying one of its four hardcoded addresses,
|
|
138
|
+
dead duplicates included. Verified on prod 2026-09-02: each of the four addresses currently
|
|
139
|
+
resolves to exactly **one** active row in **both** tenants (zero inactive), so nothing is being
|
|
140
|
+
resurrected today — but the query would happily do it if a duplicate appeared. That hardcoded list
|
|
141
|
+
is also still the **only** durable way to hold a cross-region user active, and changing it needs a
|
|
142
|
+
code change and a deploy per request.
|
|
124
143
|
|
|
125
144
|
## Remediation — BUILT 2026-08-20
|
|
126
145
|
|
|
@@ -151,6 +170,17 @@ question is now contained rather than blocking.
|
|
|
151
170
|
|
|
152
171
|
## Change history
|
|
153
172
|
|
|
173
|
+
- 2026-09-04 — Recorded that the inactivation sweep in `executeForTenant()` is the **only**
|
|
174
|
+
`isActive = 0` writer in the whole class (every other path only forces it back to 1), so an
|
|
175
|
+
activation made outside the cron is **temporary** while a deactivation is **durable** — the
|
|
176
|
+
governing constraint on the new [Tools Compass admin
|
|
177
|
+
pages](../../../1.0/apps/tools/features/compass-user-persona-admin.md). Noted that
|
|
178
|
+
`ensureCrossRegionUsersActive()` filters on email only, so it would reactivate dead duplicates
|
|
179
|
+
too (prod check 2026-09-02: one active row per address in both tenants, so nothing is being
|
|
180
|
+
resurrected today), and that its hardcoded four-address list remains the only durable
|
|
181
|
+
cross-region hold. Extended the missing-index gotcha with `c_hrEmpUsername`, the `EXPLAIN`
|
|
182
|
+
evidence, and the fact that the blank-client template's `UNIQUE KEY email` does **not** describe
|
|
183
|
+
this tenant. (ajean)
|
|
154
184
|
- 2026-08-20 — **Built the remediation** that this doc had recorded as designed-but-unbuilt: the
|
|
155
185
|
importer is now a thin trigger that collects newly created user ids and calls the new worker action
|
|
156
186
|
`Client/Compass/ApprovalReassignment/Reassign` **after the tenant transaction commits**; all the
|
|
@@ -6,12 +6,13 @@ project: Worker
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [bala]
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: [bala, ajean]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Client/Compass/PeopleFile.php
|
|
13
13
|
related:
|
|
14
14
|
- ./people-file-user-lifecycle.md
|
|
15
|
+
- ../../../2.0/apps/_underscore/features/effective-persona-resolution.md
|
|
15
16
|
- ../profile.md
|
|
16
17
|
- ../workflows/persona-refactor-migration.md
|
|
17
18
|
- ../workflows/persona-population-env-comparison.md
|
|
@@ -95,6 +96,13 @@ hierarchy root**:
|
|
|
95
96
|
└─ … (users sit ~5 levels below 55)
|
|
96
97
|
```
|
|
97
98
|
|
|
99
|
+
> **How a user's personas are *read* is a different rule from how this cron *assigns* them.** The
|
|
100
|
+
> effective set is a UNION of `Users_Personas`, the user's own `Locations_Personas`, and their
|
|
101
|
+
> sector's — and on prod **67,157 of 74,268** active Compass users get theirs from the **sector**,
|
|
102
|
+
> not from a row of their own. See
|
|
103
|
+
> [Effective persona resolution](../../../2.0/apps/_underscore/features/effective-persona-resolution.md)
|
|
104
|
+
> before writing any "who has persona N" query.
|
|
105
|
+
|
|
98
106
|
## Persona assignment, by tenant
|
|
99
107
|
|
|
100
108
|
**US** (`$tenantKey === 'US'`):
|
|
@@ -138,6 +146,12 @@ VIP (35) and CDL (30) handling is orthogonal and applies to both tenants unchang
|
|
|
138
146
|
constants from this file into tickets, docs, or chat transcripts.
|
|
139
147
|
|
|
140
148
|
## Change history
|
|
149
|
+
- 2026-09-04 — Linked the framework rule for **reading** effective personas
|
|
150
|
+
([Effective persona resolution](../../../2.0/apps/_underscore/features/effective-persona-resolution.md)):
|
|
151
|
+
`_Model_Client_User::_personaIds()` unions `Users_Personas`, the user's own `Locations_Personas`
|
|
152
|
+
and their sector's, and on prod 2026-09-02 **67,157 of 74,268** active Compass users resolve
|
|
153
|
+
through the **sector** (Canada: 4,029 of 4,948). Assignment by this cron and resolution at read
|
|
154
|
+
time are separate rules — do not answer "who has persona N" from `Users_Personas`. (ajean)
|
|
141
155
|
- 2026-08-26 — **Correction from a read-only prod check.** The "Non-Levy = id **38** on dev-sandbox
|
|
142
156
|
*and* prod" claim is wrong: prod has **no Non-Levy persona** and id 38 is **"MyDining- KDS"**, so
|
|
143
157
|
prod's auto-increment is past 38 and persona 1 is still named **"All"**. Reinforces resolve-by-name.
|
|
@@ -15,12 +15,13 @@ apps:
|
|
|
15
15
|
- library
|
|
16
16
|
- test
|
|
17
17
|
- talos-backend
|
|
18
|
+
- tools
|
|
18
19
|
project: _Underscore
|
|
19
20
|
client: compass-usa
|
|
20
21
|
type: profile
|
|
21
22
|
status: active
|
|
22
|
-
updated: 2026-09-
|
|
23
|
-
owners: [jcardinal, bala, tcox, apeterson, dfranks, akhokhani]
|
|
23
|
+
updated: 2026-09-04
|
|
24
|
+
owners: [jcardinal, bala, tcox, apeterson, dfranks, akhokhani, ajean]
|
|
24
25
|
files: []
|
|
25
26
|
related:
|
|
26
27
|
- ../../2.0/apps/_underscore/features/sales-order-po-number-sourcing.md
|
|
@@ -6,13 +6,15 @@ project: Database Changes
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: [bala]
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: [bala, ajean]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
|
|
13
13
|
related:
|
|
14
14
|
- ./granting-navigation-access.md
|
|
15
15
|
- ./changing-a-kit-line-item.md
|
|
16
|
+
- ../../../2.0/apps/_underscore/features/effective-persona-resolution.md
|
|
17
|
+
- ../../../1.0/apps/tools/features/compass-user-persona-admin.md
|
|
16
18
|
- ../features/persona-model-and-levy-gating.md
|
|
17
19
|
- ./persona-refactor-migration.md
|
|
18
20
|
- ./persona-population-env-comparison.md
|
|
@@ -139,6 +141,11 @@ A user's **effective** persona set is three UNIONed sources, not just `Users_Per
|
|
|
139
141
|
3. `Locations_Personas` on the user's **sector** — five `parentLocationId` hops up
|
|
140
142
|
(location → complex → district → region → division → sector).
|
|
141
143
|
|
|
144
|
+
The framework already implements exactly this in `_Model_Client_User::_personaIds()` — mirror it
|
|
145
|
+
rather than re-deriving it, and note that on prod the **sector** hop is where almost every Compass
|
|
146
|
+
user gets their personas. Full rule and the measured numbers:
|
|
147
|
+
[Effective persona resolution](../../../2.0/apps/_underscore/features/effective-persona-resolution.md).
|
|
148
|
+
|
|
142
149
|
**Check all three before adding anything** — the user may already reach the bundle through their
|
|
143
150
|
location, in which case the right answer is "no change needed". After the grant, re-run the same
|
|
144
151
|
resolution and confirm the bundle number appears; the verification block at the end of the template
|
|
@@ -167,6 +174,12 @@ tooling uses — see
|
|
|
167
174
|
removed. A "remove access" request is a different, destructive job and needs its own review.
|
|
168
175
|
|
|
169
176
|
## Change history
|
|
177
|
+
- 2026-09-04 — Pointed the three-source verification step at the framework's own implementation,
|
|
178
|
+
`_Model_Client_User::_personaIds()`, now documented with prod measurements in
|
|
179
|
+
[Effective persona resolution](../../../2.0/apps/_underscore/features/effective-persona-resolution.md).
|
|
180
|
+
The Tools [Compass admin pages](../../../1.0/apps/tools/features/compass-user-persona-admin.md)
|
|
181
|
+
now do the "grant a persona to a user" step (`Users_Personas` insert) in a browser; the bundle /
|
|
182
|
+
item / assortment work in this file still needs SQL. (ajean)
|
|
170
183
|
- 2026-09-02 — Recorded the **reverse case** and cross-linked it: adding a new line to a kit that
|
|
171
184
|
personas *already* own hits the same item-ACL rule, so a fully active `BundleItems` row is omitted
|
|
172
185
|
per-persona until `Personas_Items` is backfilled (item 687 `TabAdminSuppPro` reached only MITS Team
|
package/package.json
CHANGED